模块

模块(Module)是自动运行在严格模式下并且没有办法退出运行的JavaScript代码。

  • 模块的代码自动运行在严格模式下
  • 模块的顶部,this的值是undefined

导出语法

可以用export关键字将任意变量、函数或类声明从模块中导出。除非用default关键字,否则不能用export导出匿名函数或类

1export const color = 'red'
2
3function multiply(factor, faciend){
4    return factor * faciend
5}
6
7export multiply

导入语法

模块的导出可以通过import关键字在另一个模块中访问。

1import {color, multiply} from './example.js'

import后面的大括号表示从给定模块导入的绑定(binding)。

导入绑定的列表看起来和解构对象很像,但它不是

关键字from表示从哪个模块导入。由表示模块路径的字符串指定。

  • 浏览器使用的路径格式与传给<script>元素的相同,必须加上扩展名
  • Node.js则遵循基于文件系统前缀区分本地文件和包的习惯

从模块中导入的绑定,和常量const类似,不能存在同名变量,也无法在import语句前使用标识符或改变绑定的值

可以使用as关键字将整个模块作为一个单一对象导入。该模块的所有导出都可以作为对象的属性使用。

1import * as example from './example.js'
2
3console.log(example.multiply(1, 2)) // 2

一个模块不管被import了几次,都只执行一次。

exportimport的一个重要限制是必须在其他语句和函数之外使用

导出和导入时重命名

当导入或者导出变量、函数或者类时,可以用as关键字改变名称。

1import {color as copiedColor} from './example.js' // 不能用解构赋值语法 import {color: copiedColor} from './example.js'
2
3function sum(a, b){
4    return a + b
5}
6
7export {
8    sum as add
9}

模块的默认值

模块的默认值是指通过default关键字指定的单个变量、函数或类。只能为每个模块设置一个默认导出值。

1export const name = 'Mike'
2export default function (a, b){
3    return a + b
4}
5
6// 另一个模块
7import add, {name} from './add.js'

另外还可以通过重命名来导出默认值,上例可以改成:

1const name = 'Mike'
2function add (a, b){
3    return a + b
4}
5export {
6    add as default,
7    name
8}
9
10// 另一个模块
11import {
12    default as add,
13    name
14} from './add.js'

重新导出一个绑定

1export * from './example.js'
2export {
3    default as add,
4    name
5} from './add.js'

加载模块

ES6定义了模块语法,但是并没有定义如何加载这些模块。加载机制由一个未定义的内部抽象方法HostResolveImportedModule决定,浏览器和Node.js可以自己实现。

<script>中将type设置为module时,支持加载模块。为了保证模块的加载顺序,<script type="module">在执行时,自动应用defer属性。因此,所以的模块组件在文档被解析完才会执行。

由于每个模块都可以从其他模块导入,因此在加载阶段,该模块加载完之后会识别所有导入语句,然后每个导入语句都出发一次获取过程,并且在所有导入资源都被加载之后,执行当前模块。

以如下代码为例:

1<!-- 先执行这个标签 -->
2<script type="module" src="module1.js"></script>
3<!-- 再执行这个标签 -->
4<script type="module">
5import {multiply} from './example.js'
6
7const result = multiply(1, 3)
8</script>
9<!-- 最后执行这个标签 -->
10<script type="module" src="module2.js"></script>

完整的加载顺序如下:

  1. 下载并解析module1.js
  2. 递归下载并解析module1.js中导入的模块
  3. 解析内联模块
  4. 递归下载并解析内联模块中导入的模块
  5. 下载并解析module2.js
  6. 递归下载并解析module2.js中导入的模块

加载完成之后,只有当文档完全被解析之后才会执行以下操作:

  1. 递归执行module1.js中导入的模块
  2. 执行module1.js
  3. 递归执行内联模块中导入的模块
  4. 执行内联模块
  5. 递归执行module2.js中导入的模块
  6. 执行module2.js

异步模块加载

与通用脚本加载一样,模块也支持async属性,设置之后,会以异步方式加载。异步加载的模块不必等待文档解析完成,但是需要模块中所有导入文件都加载完成,才会执行模块。但是无法保证模块的先后执行顺序,而是哪个模块及其依赖模块先加载完就先执行哪个模块。

将模块作为Worker加载

通过配置Worker的第二个参数,可以支持以模块方式加载。

1const worker = new Worker('script.js') // 创建的Worker以脚本方式加载
2
3const moduleWorker = new Worker('module.js', {type: 'module'}) // 创建的Worker以模块方式加载

以脚本方式加载的Worker与以模块方式加载的Worker存在以下两点不同:

  1. Worker脚本只能引用与网页同源的JavaScript,而Worker模块不会完全受限,可以加载并访问具有适当的跨域资源共享(CORS)头的文件。
  2. Worker脚本可以使用self.importScripts()加载其他脚本,但Worker模块不能,而是应该使用import来导入。

浏览器模块说明符解析

在浏览器中,模块说明符(module specifier)只支持以下四种格式:

  • /开头,从根目录开始解析
  • ./开头,从当前目录开始解析
  • ../开头,从父级目录开始解析
  • URL格式,不同源时,需要正确配置跨域(CORS)

以下的格式,是无效的,并且会导致错误

1import {multiply} from 'example.js'

ES Modules进阶