# 模块

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

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

# 导出语法

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

export const color = 'red'

function multiply(factor, faciend){
    return factor * faciend
}

export multiply
1
2
3
4
5
6
7

# 导入语法

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

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

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

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

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

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

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

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

import * as example from './example.js'

console.log(example.multiply(1, 2)) // 2
1
2
3

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

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

# 导出和导入时重命名

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

import {color as copiedColor} from './example.js' // 不能用解构赋值语法 import {color: copiedColor} from './example.js'

function sum(a, b){
    return a + b
}

export {
    sum as add
}
1
2
3
4
5
6
7
8
9

# 模块的默认值

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

export const name = 'Mike'
export default function (a, b){
    return a + b
}

// 另一个模块
import add, {name} from './add.js'
1
2
3
4
5
6
7

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

const name = 'Mike'
function add (a, b){
    return a + b
}
export {
    add as default,
    name
}

// 另一个模块
import {
    default as add,
    name
} from './add.js'
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 重新导出一个绑定

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

# 加载模块

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

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

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

以如下代码为例:

<!-- 先执行这个标签 -->
<script type="module" src="module1.js"></script>
<!-- 再执行这个标签 -->
<script type="module">
import {multiply} from './example.js'

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

完整的加载顺序如下:

  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的第二个参数,可以支持以模块方式加载。

const worker = new Worker('script.js') // 创建的Worker以脚本方式加载

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

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

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

# 浏览器模块说明符解析

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

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

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

import {multiply} from 'example.js'
1

# ES Modules进阶