EsModule 是 Es6 中提供的一种新的模块化语法标准。
采用了 EsModule 的代码会自动启用严格模式 use strict。
导出使用 export 或 export default 语法。
导入使用 import from 或者 import() 语法。
TIP
export、export default、import from 语法可以放在顶层代码的任何位置,但不能放在块级作用域内。
因为 EsModule 是在编译阶段执行的,在代码运行之前。
这将不利于 EsModule 的静态分析,违背了 EsModule 的设计准则。
通常,在实际开发中,为了代码的可读性和可维护性,我们建议将 import from 语法放置在文件头部,而 export 相关语法放在文件底部。
7-1.export
export 用来导出模块。
7-1-1.命名导出
命名导出,顾名思义,指的是导出的是命名形式。
也就是说,这种形式的导出,必须要有名字。这样的话,导出与名字有了绑定关系,导入时才能获取到具体的导出。
因此,导出一个值或者匿名函数声明,都是不行的。
- 可以导出一个变量,该变量绑定值。
- 可以导出一个命名函数声明或者函数表达式。
在阮一峰老师的ECMAScript 6 入门中,将这里的 export 看做是导出一个接口,但并没有对接口的含义加以详细解释即剖析。
因此,上述的加粗重点文字是笔者个人的理解,方便本人记忆。
以下是能够正常执行的命名导出形式:
var key = 'foo'
// 1.导出一个变量
export var name = 'Jack'
// 2.导出一个函数
export function getName () {
return name
}
// 3.导出一个class
export class MyClass {}
// 4.虽然类似对象的写法,但并不是导出对象,可以看做导出一个集合
export {
key
}以下是不能够正常执行的命名导出形式:
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 进行重命名:
var v1 = 'Bruce Wayne'
var v2 = 'Clark Kent'
export {
v1: BatMan,
v2: SuperMan,
v2: IronHero
}对应的,在导入的时候,将不会使用 v1、v2,而是使用新的名字。
正如上述代码,可以利用 as 将同一变量进行多次重命名导出。
7-1-3.默认导出
关于默认导出,需要提前说明的两点是:
TIP
默认导出相当于导出名已设置为
default的命名导出。默认导出在同一个文件内只能调用一次。
var name = 'Jack'
// ①
export default name
// ②
export {
name as default
}这导致的结果是在使用默认导出时,我们无需再指定导出名。
在使用默认导出时,指定导出名,属于画蛇添足。程序并不能正常执行。
以下是能够正常执行的默认导出形式:
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
}以下是不能够正常执行的默认导出形式:
var key = 'foo'
// 1.导出一个变量
export default var name = 'Jack'
// 2.导出一个函数表达式
export default const getName = function () {
return name
}
// 3.导出一个class表达式
export default const MyClass = class {}比较特殊的一点是,可以默认导出函数声明,但不能默认导出函数表达式:
// 1.导出一个函数声明✅
export default function getName () {
return 'Jack'
}
// 2.导出一个class声明✅
export default class MyClass {}7-1-4.重新导出
重新导出的应用场景,大多是在引用包的时候,该文件只作中转作用。
实际含义,就是导入包的时候,同时直接重新导出包。
从下文中的导入方式,通常有以下几种:
// 命名导入
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'那么对于重新导出,上面的对应语句为:
// 重新导出-命名导入
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 语法是在编译阶段执行的,在代码运行之前。所以下述代码能够正常执行:
foo()
import { foo } from 'my_module'因为 import { foo } from 'my_module' 被提前解析,这样的话,调用 foo() 时已经能够拿到 foo 这个变量了。
静态 import 语法能够放在模块顶层代码的任何位置,不能放置在块级作用域内。
另外,import 导入的变量相当于 const 声明的常量,不能更改。
7-2-1.命名导入
命名导入是与命名导出相对应的。
// 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 是有区别的。譬如:
// 这里,CommonJS是作了全量加载,然后利用解构,只获取指定的部分
var { name, getName } = require('path/to/module')// 这里,EsModule是作了按需加载,会利用静态分析,只加载指定的部分
import { name, getName } from 'path/to/module'7-2-2.别名导入
利用命名导入时,我们也可以对其进行重命名:
// 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())另外也可以全量导入:
// 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())但实际开发中,我们并不推荐这种形式。因为它违背了按需导入的设计准则,不利于 webpack 的 tree shaking。
7-2-3.默认导入
默认导入与默认导出是相对应的。
// 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())默认导入,也可以与命名导入联用:
// 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() 语法,但二者也有区别:
require()是同步加载,import()是异步加载。require()返回的是模块(具体类型以导出为准),import()返回的是Promise实例。
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 的话,有两种方式:
- 将
.js的后缀名修改为.mjs。 - 在
package.json中声明type为module。
{
"type": "module"
}TIP
ES6 模块与 CommonJS 模块尽量不要混用。
require 命令不能加载 .mjs 文件,会报错,只有 import 命令才可以加载 .mjs 文件。
反过来,.mjs 文件里面也不能使用 require 命令,必须使用 import。
7-5-1.CommonJS 模块加载 EsModule 模块
譬如:
// 这里利用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,内容如下:
module.exports = {
name: 'Understand Es6',
author: 'ruanyifeng'
}可以这样利用 EsModule 导入:
// work
import info from 'path/to/utils.js'
console.log(info.name)
console.log(info.author)也能采用命名导入的方式:
// work
import { name, author } from 'path/to/utils.js'
console.log(name)
console.log(author)7-6.EsModule与CommonJS的区别
EsModule 与 CommonJS 的区别在于:
CommonJS模块是运行时加载,EsModule模块是编译时加载。CommonJS模块导出的是值的拷贝,EsModule模块导出的是值的引用。CommonJS模块的require()是同步加载,EsModule模块的import()是异步加载。
其中,第 1、3 点在上文中已经有了相关介绍,本节不做过多赘述。
我们重点验证下第 2 点。
在 CommonJs 模块系统下测试,可以发现模块内部的值改变时,导出的值不会改变:
// 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 模块系统下测试,可以发现模块内部的值改变时,导出的值也会改变:
// 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) // Jerry7-7.循环依赖
循环依赖,指的是模块之间互相导入、互相依赖。
正如某个哲学问题,世界上是先有鸡,还是先有蛋,一旦纠缠结果,这就是循环不断的命题。
所以,一般对此的处理方法,要掐断这种循环逻辑。
CommonJS 与 EsModule 的处理方式类似,但是由于二者的模块化特点,具体细节上有差别。
譬如下文中的 a 加载 b,此时 a 模块并没有完全导出,CommonJS 和 EsModule 都是去加载 b。
在 b 中加载 a 时,由于 a 模块并没有完全导出,而且 CommonJS 与 EsModule 有执行差异,所以这里的结果会有差异。
如果要我总结 CommonJS 与 EsModule 对于循环依赖的差异之处,那就是:
CommonJS 按照顺序加载,而 EsModule 提升加载。
更多细节和验证,可以分别参考下文中各节的对应例子。
7-7-1.CommonJS
在Node官方文档中,有一个例子进行说明:
// 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 执行完毕')// 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 执行完毕')// 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 的话是这样的:
// 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 执行完毕')// 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 执行完毕')// 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也只会执行一次代码,即使有循环依赖。