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) // Jerry
7-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
也只会执行一次代码,即使有循环依赖。