Skip to content

EsModuleEs6 中提供的一种新的模块化语法标准。

采用了 EsModule 的代码会自动启用严格模式 use strict

导出使用 exportexport default 语法。

导入使用 import from 或者 import() 语法。

TIP

exportexport defaultimport from 语法可以放在顶层代码的任何位置,但不能放在块级作用域内。

因为 EsModule 是在编译阶段执行的,在代码运行之前。

这将不利于 EsModule 的静态分析,违背了 EsModule 的设计准则。

通常,在实际开发中,为了代码的可读性和可维护性,我们建议将 import from 语法放置在文件头部,而 export 相关语法放在文件底部

7-1.export

export 用来导出模块。

7-1-1.命名导出

命名导出,顾名思义,指的是导出的是命名形式。

也就是说,这种形式的导出,必须要有名字。这样的话,导出与名字有了绑定关系,导入时才能获取到具体的导出

因此,导出一个值或者匿名函数声明,都是不行的。

  • 可以导出一个变量,该变量绑定值。
  • 可以导出一个命名函数声明或者函数表达式。

在阮一峰老师的ECMAScript 6 入门中,将这里的 export 看做是导出一个接口,但并没有对接口的含义加以详细解释即剖析。

因此,上述的加粗重点文字是笔者个人的理解,方便本人记忆。

以下是能够正常执行的命名导出形式

js
var key = 'foo'

// 1.导出一个变量
export var name = 'Jack'

// 2.导出一个函数
export function getName () {
  return name
}

// 3.导出一个class
export class MyClass {}

// 4.虽然类似对象的写法,但并不是导出对象,可以看做导出一个集合
export {
  key
}

以下是不能够正常执行的命名导出形式

js
var key = 'foo'

// 1.不能导出一个值 因为这里相当于export 'Jack',导入的时候没法用
var name = 'Jack'
export name

// 2.不能导出匿名函数
export function () {
  return name
}

// 3.导出一个未命名的class
export class {}

// 4.这里并不是导出对象 所以这种对象的写法并不会支持
export {
  key: 'bar'
}

7-1-2.别名导出

在导出的时候,也可以利用 as 进行重命名:

js
var v1 = 'Bruce Wayne'
var v2 = 'Clark Kent'

export {
  v1: BatMan,
  v2: SuperMan,
  v2: IronHero 
}

对应的,在导入的时候,将不会使用 v1v2,而是使用新的名字。

正如上述代码,可以利用 as 将同一变量进行多次重命名导出。

7-1-3.默认导出

关于默认导出,需要提前说明的两点是:

TIP

  1. 默认导出相当于导出名已设置为 default 的命名导出

  2. 默认导出在同一个文件内只能调用一次

js
var name = 'Jack'
// ①
export default name
// ②
export {
  name as default
}

这导致的结果是在使用默认导出时,我们无需再指定导出名。

在使用默认导出时,指定导出名,属于画蛇添足。程序并不能正常执行。

以下是能够正常执行的默认导出形式

js
var key = 'foo'
var name = 'Jack'

// 1.导出一个变量
export default name

// 2.导出一个函数
export default function () {
  return name
}

// 3.导出一个class
export default class {}

// 4.导出一个对象
export default {
  key
}

以下是不能够正常执行的默认导出形式

js
var key = 'foo'

// 1.导出一个变量
export default var name = 'Jack'

// 2.导出一个函数表达式
export default const getName = function () {
  return name
}

// 3.导出一个class表达式
export default const MyClass = class {}

比较特殊的一点是,可以默认导出函数声明,但不能默认导出函数表达式

js
// 1.导出一个函数声明✅
export default function getName () {
  return 'Jack'
}

// 2.导出一个class声明✅
export default class MyClass {}

7-1-4.重新导出

重新导出的应用场景,大多是在引用包的时候,该文件只作中转作用

实际含义,就是导入包的时候,同时直接重新导出包。

从下文中的导入方式,通常有以下几种:

js
// 命名导入
import { moduleA } from './path/to/module'

// 别名导入
import { moduleA as a } from './path/to/module'
import * as totalModule from './path/to/module' // 这里的 * 会包含所有的命名导出和默认导出

// 默认导入
import myModule from './path/to/module'

那么对于重新导出,上面的对应语句为:

js
// 重新导出-命名导入
export { moduleA } from './path/to/module'

// 重新导出-别名导入
export { moduleA as a } from './path/to/module'
export * as totalModule from './path/to/module'

// 重新导出-默认导入
export { default } from './path/to/module'

7-2.静态import

静态 import 指的是 import ... from '' 语法。

静态 import 语法是在编译阶段执行的,在代码运行之前。所以下述代码能够正常执行:

js
foo()

import { foo } from 'my_module'

因为 import { foo } from 'my_module' 被提前解析,这样的话,调用 foo() 时已经能够拿到 foo 这个变量了。

静态 import 语法能够放在模块顶层代码的任何位置,不能放置在块级作用域内

另外,import 导入的变量相当于 const 声明的常量,不能更改

7-2-1.命名导入

命名导入是与命名导出相对应的。

js
// utils.js
export var name = 'Jack'
export function getName () {
  return name
}

// main.js
import { name, getName } from 'path/to/utils.js'
console.log(name)
console.log(getName())

需要说明的是,这种形式并不是利用了 Es6 中的解构赋值

这与 CommonJS 是有区别的。譬如:

js
// 这里,CommonJS是作了全量加载,然后利用解构,只获取指定的部分
var { name, getName } = require('path/to/module')
js
// 这里,EsModule是作了按需加载,会利用静态分析,只加载指定的部分
import { name, getName } from 'path/to/module'

7-2-2.别名导入

利用命名导入时,我们也可以对其进行重命名:

js
// utils.js
export var name = 'Jack'
export function getName () {
  return name
}

// main.js
import { name as myName, getName as getMyName } from 'path/to/utils.js'
console.log(myName)
console.log(getMyName())

另外也可以全量导入:

js
// utils.js
export var name = 'Jack'
export function getName () {
  return name
}

// main.js
import * as myModule from 'path/to/utils.js'
console.log(myModule.name)
console.log(myModule.getName())

但实际开发中,我们并不推荐这种形式。因为它违背了按需导入的设计准则,不利于 webpacktree shaking

7-2-3.默认导入

默认导入与默认导出是相对应的。

js
// utils.js
export var name = 'Jack'
export function getName () {
  return name
}
export default function () {
  return new Date()
}

// main.js
import getTime from 'path/to/utils.js'
console.log(getTime())

默认导入,也可以与命名导入联用:

js
// utils.js
export var name = 'Jack'
export function getName () {
  return name
}
export default function () {
  return new Date()
}

// main.js 注意这里的导入顺序
import getTime, { name, getName } from 'path/to/utils.js'

console.log(name)
console.log(getName())
console.log(getTime())

但要注意的是,默认导入与命名导入联用时,导入顺序是默认导入在前,命名导入在后,用逗号 , 分隔

7-3.动态import

前文中,我们提到静态 import 不能放在块级作用域内。

但实际开发中,难以避免需要 require 这种形式的运行时动态加载。

于是有人提出了 import() 语法。

它在使用形式类似于 CommonJS 中的 require() 语法,但二者也有区别:

  1. require() 是同步加载,import() 是异步加载。
  2. require() 返回的是模块(具体类型以导出为准),import() 返回的是 Promise 实例。
js
import('path/to/utils.js').then(data => {
  console.log(data)
})

打印结果如下:

7-4.浏览器中的EsModule

现代浏览器,已经逐渐开始支持直接在 <script> 标签内使用 EsModule

如果要使用 import from 这样的静态导入语法,需要声明 <script> 标签的 type 属性为 module

否则会报错 Uncaught SyntaxError: Cannot use import statement outside a module

而如果要使用 import() 这样的动态导入语法,则不需要声明。

关于 type 属性的更多信息可见本站的script标签的type属性

7-5.Node中的EsModule

高版本的 Node 也开始支持 EsModule

如果想要在 Node 环境下正常执行 EsModule 的话,有两种方式:

  1. .js 的后缀名修改为 .mjs
  2. package.json 中声明 typemodule
json
{
  "type": "module"
}

TIP

ES6 模块与 CommonJS 模块尽量不要混用。

require 命令不能加载 .mjs 文件,会报错,只有 import 命令才可以加载 .mjs 文件。

反过来,.mjs 文件里面也不能使用 require 命令,必须使用 import

7-5-1.CommonJS 模块加载 EsModule 模块

譬如:

js
// 这里利用CommonJS加载EsModule
var name = require('path/to/utils.mjs')

运行会报错。

CommonJS 模块并不能加载 EsModule 模块。

只能采用 EsModule 的模块化标准加载 EsModule 模块。

仔细思考的话🤔,CommonJS 模块化标准是很早之前出来的,而 EsModule 属于近期的后期之秀。

EsModule 本身有静态分析、命名导出、默认导出等等特点。

所以 CommonJS 的设计并不能考虑到现在 EsModule 的特点,也就是说,CommonJS 不兼容 EsModule 标准。

7-5-2.EsModule 模块加载 CommonJS 模块

值得一提的是,EsModule 模块可以加载 CommonJS 模块。

在阮一峰的深入理解Es6中,提到:

ES6 模块的import命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。

笔者测试时,发现 EsModule 既能整体加载,又能只加载单一的输出项

测试时 Node 版本为 v14.16.0

譬如,有一个模块文件 utils.js,内容如下:

js
module.exports = {
  name: 'Understand Es6',
  author: 'ruanyifeng'
}

可以这样利用 EsModule 导入:

js
// work
import info from 'path/to/utils.js'
console.log(info.name)
console.log(info.author)

也能采用命名导入的方式:

js
// work
import { name, author } from 'path/to/utils.js'
console.log(name)
console.log(author)

7-6.EsModule与CommonJS的区别

EsModuleCommonJS 的区别在于:

  1. CommonJS 模块是运行时加载,EsModule 模块是编译时加载。
  2. CommonJS 模块导出的是值的拷贝,EsModule 模块导出的是值的引用。
  3. CommonJS 模块的 require() 是同步加载,EsModule 模块的 import() 是异步加载。

其中,第 13 点在上文中已经有了相关介绍,本节不做过多赘述。

我们重点验证下第 2 点。

CommonJs 模块系统下测试,可以发现模块内部的值改变时,导出的值不会改变

js
// utils.js
var name = 'Tom'
function setName (val) {
  name = val
}
module.exports = {
  name,
  setName
}

// main.js
var utils = require('path/to/utils.js')
console.log(utils.name) // Tom
setName('Jerry')
console.log(utils.name) // Tom

EsModule 模块系统下测试,可以发现模块内部的值改变时,导出的值也会改变

js
// utils.js
var name = 'Tom'
function setName (val) {
  name = val
}
export {
  name,
  setName
}

// main.js
import { name, setName } from 'path/to/utils.js'
console.log(name) // Tom
setName('Jerry')
console.log(name) // Jerry

7-7.循环依赖

循环依赖,指的是模块之间互相导入、互相依赖。

正如某个哲学问题,世界上是先有鸡,还是先有蛋,一旦纠缠结果,这就是循环不断的命题。

所以,一般对此的处理方法,要掐断这种循环逻辑。

CommonJSEsModule 的处理方式类似,但是由于二者的模块化特点,具体细节上有差别。

譬如下文中的 a 加载 b此时 a 模块并没有完全导出CommonJSEsModule 都是去加载 b

b 中加载 a 时,由于 a 模块并没有完全导出,而且 CommonJSEsModule 有执行差异,所以这里的结果会有差异。

如果要我总结 CommonJSEsModule 对于循环依赖的差异之处,那就是:

CommonJS 按照顺序加载,而 EsModule 提升加载

更多细节和验证,可以分别参考下文中各节的对应例子。

7-7-1.CommonJS

Node官方文档中,有一个例子进行说明:

js
// a.js
exports.done = false
var b = require('./b.js')
console.log('在 a.js 之中,b.done = %j', b.done)
exports.done = true
console.log('a.js 执行完毕')
js
// b.js
exports.done = false
var a = require('./a.js')
console.log('在 b.js 之中,a.done = %j', a.done)
exports.done = true
console.log('b.js 执行完毕')
js
// main.js
console.log('main start')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done = %j, b.done = %j', a.done, b.done)

执行 main.js 之后,打印依次是:

main start
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
in main, a.done = true, b.done = true

可以得出的结论是:

  • require 并没有提升效果。它按照写在代码处的位置,触发执行。
  • require 遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值
  • require 是同步加载。依赖加载从上至下,层层分析加载。
  • require 只会执行一次代码,即使有循环依赖,后续加载会从缓存中获取导出。

7-7-2.EsModule

参照 CommonJS 的例子,修改为 EsModule 的话是这样的:

js
// a.mjs
export var a1 = false
import { b1, b2 } from './b.mjs'
console.log('在 a.mjs 之中,', b1, b2)
export var a2 = true
console.log('a.mjs 执行完毕')
js
// b.mjs
export var b1 = false
import { a1, a2 } from './a.mjs'
console.log('在 b.mjs 之中,', a1, a2)
export var b2 = true
console.log('b.mjs 执行完毕')
js
// main.mjs
console.log('main start')
import { a1, a2 } from './a.mjs'
import { b1, b2 } from './b.mjs'
console.log('in main,', a1, a2, b1, b2)

执行 main.js 之后,打印依次是:

在 b.mjs 之中, undefined undefined
b.mjs 执行完毕
在 a.mjs 之中, false true
a.mjs 执行完毕
main start
in main, false true false true

可以看出,EsModule 有自己的特点(以下 import 均指 import ... from ...):

  • import 具有提升效果。它会提升到代码头部,优先执行
  • import 在模块中一定是最先执行的,即使它写在代码下方。这样就导致循坏依赖中,会导出 undefined
  • import 也是同步加载。依赖加载从上至下,层层分析加载。
  • import 也只会执行一次代码,即使有循环依赖。