普通的 JS 文件没有模块作用域,没有边界,共享一个全局作用域,非常容易变量冲突,难以维护。所以引入模块,一个模块就是一个文件ES6
区别
模块化文件相比普通脚本的区别:
- 默认严格模式
- 每个模块都有自己的顶级作用域
- 如果同一个模块被多次导入,那么它的代码只会在第一次被导入时执行一次
- 模块只能通过 HTTP(s) 引入,不能从本地文件引入
- this 是 undefined
- 在浏览器中
- 使用模块化脚本需要添加 type=“module”:
<script type="module">
- 总是延时加载的,类似
defer
- 外部和内联模块脚本都可以添加
async
(比 defer 优先级高;且普通脚本只能外部引入才能添加 async) - 从另一个源加载的外部脚本需要 CORS header
- 使用模块化脚本需要添加 type=“module”:
模块脚本 | 普通脚本 | |
---|---|---|
严格模式 | 默认严格模式 | 默认非严格模式 |
作用域 | 有自己的顶级作用域 | 全局作用域 |
多次导入 | 只会在第一次被导入时执行一次 | 会执行多次 |
顶层this | undefined | window(严格和非严格都是) |
import.meta 对象包含关于当前模块的信息ES2020
导出(export)
export 可以出现在任何位置,只要在顶层作用域就行,如果处于块级作用域内,就会报错(没法做静态优化)
没有任何 import 或者 export,但是希望它被作为模块处理可以在后面加上 export {}
1. 声明时导出
export const VALUE = 1
export function hello() {
console.log('hello world')
}
2. 导出和声明分开
function hello() {
console.log('hello world')
}
export { hello } // 导出变量列表,加大括号
3. 导出别名
function hello() {
console.log('hello world')
}
export { hello as h } // 使用 as
4. 默认导出
一个模块只能有一个默认导出,默认导出可以不要变量名称(其他文件引入时使用 default 名称),不用大括号
本质上,export default 就是输出一个叫做 default 的变量或方法
export default function foo() {
console.log('hello world')
}
// 或者
function foo() {
console.log('hello world')
}
export default foo
// 或者
function foo() {
console.log('hello world')
}
export { foo as default }
5. 重新导出
导入内容,并立即将其导出,一般用于将多个文件的导出进行汇总到 index 然后再统一导出
重新导出的模块在当前文件中不可用
export { a } from './a.js' // 重新导出命名导出
// 类似下面,但是没有实际导入
import { a } from './a.js'
export { a }
export * from './b.js' // 重新导出所有命名导出(不含默认导出)
export { default } from './c.js' // 重新导出默认导出
export { default as m } from './d.js' // 重新将默认导出为命名导出
export B from './b.js' // error! 这是一个错误语法(不能直接这样默认导出,只能按上面那样)
导入(import)
import 必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为“裸(bare)”模块,在 import 中不允许这种模块。(打包工具可以)
1. 直接导入
不会引用任何变量到当前模块,但是会执行导入的文件中的所有代码
import './file.js'
2. 单个导入
import { hello } from 'a.js'
hello()
3. 全部导入
import * as A from 'a.js'
A.hello() // 引入的命名导出
A.default // 引入的默认导出
4. 别名导入
import { hello as h } from 'a.js'
h()
5. 默认导入
默认导入不需要大括号,同时可以自定义名称
import myName from 'a/js'
myName()
Tip
- import 命令具有提升效果,会提升到整个模块的头部,在编译阶段执行,在代码运行之前,由于 import 是静态执行,所以不能使用表达式和变量
- 导入对象可以改写里面的属性,并且其他模块也可以读到改写后的值。可以用于配置一个公共变量,但是这种写法很难查错
动态导入ES2020
上面的语法都是静态导入,语法简单且严格
动态按需导入模块使用 import(module)
:加载模块并返回一个 promise,该 promise 的 resolve 为一个包含其所有导出的模块对象
let { hi, bye } = await import('./a.js') // 命名导入
hi()
bye()
let obj = await import('./a.js')
let a = obj.default // 默认导入
完整示例:
// say.js
export function hi() {
alert(`Hello`);
}
export function bye() {
alert(`Bye`);
}
export default function() {
alert("Module loaded (export default)!");
}
<!doctype html>
<script>
async function load() {
let say = await import('./say.js');
say.hi(); // Hello!
say.bye(); // Bye!
say.default(); // Module loaded (export default)!
}
</script>
<button onclick="load()">Click me</button>
Tip
import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用
主要用途:
- 按需加载
- 条件加载
- 动态的模块路径
注意:
import()
并不是一个函数调用,只是一种特殊语法
模块比较
区别 | ES6 Module(ESM) | CommonJS(CJS) |
---|---|---|
语法 | import 和 export | require() 和 module.exports |
导出值 | 输出的是值的引用(原始值变了,import 加载的值也会跟着变) | 输出的是一个值的拷贝(模块内部和外部互不影响) |
加载机制 | 编译时输出接口 (静态) | 运行时加载(动态) |
加载方式 | import 命令是异步加载 | require() 是同步加载 |