Import/Export 与 Require/Export
我们都知道一开始 JavaScript 没有内置的模块化支持,于是在 ES6 modules 出现之前,JavaScript 的社区中出现了 require/export
规范,这些规范是社区中的开发者们自己拟定的规范,得到了大家广泛的承认和使用。比较知名的有 CommonJS,AMD。
- CommonJS: 这个标准主要应用在 Node.js,Browserify,Webpack。
- AMD: 这个标准最流行的实现是 RequireJS
AMD 的生命相对比较短,CommonJS 作为 Node.js 的模块化规范,一直沿用至今。直到 ES6 的出现,JavaScript 才提出了自己的标准模块化规范 import/export
,它的目标就是取代 CommonJS 和 AMD。但是直到最近的一段时间,各个 JavaScript 引擎才开始逐渐内置支持 ES6 Modules。因此在很长一段时间里,require/export
仍然是必要的,但是多亏了 Babel 的出现,Babel 会默认把 ES6 modules 的 import/export
编译成 CommonJS 的 require/export
语法,配合 Browserify 和 Webpack 这些 CommonJS 模块打包器,这才能让我们能够在前端开发中使用 ES6 modules 语法写代码。
ES6 modules 和 CommonJS 的差异
import/export
和 require/export
有几个比较大差异
1. CommonJS 模块输出的是值拷贝,ES6 模块输出的值引用
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件 lib.js
的例子。
// lib.js
var counter = 3
function incCounter() {
counter++
}
module.exports = {
courter: counter,
incCounter: incCounter
}
// main.js
var mod = require('./lib')
console.log(mod.counter) // 3
mod.incCounter()
console.log(mod.counter) // 3
lib.js
模块加载以后,它的内部变化就影响不到输出的 mod.counter
了。这是因为 mod.counter
是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
// lib.js
var counter = 3
function incCounter() {
counter++
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
}
上面代码中,输出的 counter
属性实际上是一个取值器函数。现在再执行 main.js
,就可以正确读取内部变量counter
的变动了。
$ node main.js
3
4
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
// lib.js
export let counter = 3
export function incCounter() {
counter++
}
// main.js
import { counter, incCounter } from './lib'
console.log(counter) // 3
incCounter()
console.log(counter) // 4
另外值得注意的是,ES6 Modules 输出的是值引用,但是这个引用的地址是只读的,不能重新赋值,就好比 main.js 创造了名为 counter
和 incCounter
的 const
变量。
// main.js
import { counter, incCounter } from './lib'
counter = 4 // SyntaxError: "counter" is read-only
2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
CommonJS 加载的是一个对象(即 module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成,意味着在编译时(静态)就能确定导入和导出,而不必等到运行阶段。
// CommonJS
var lib = require('lib')
lib.someFunc() // 动态解析
通过 lib.someFunc 访问一个命名的导出意味着你必须做一个属性查找,因为它是动态的所以速度很慢。
相比之下,如果在 ES6 中导入 lib,则可以静态知道其内容并优化访问:
// ES6 Modules
import * as lib from 'lib'
lib.someFunc() // 静态解析
ES6 Modules 的目标
支持同步加载和异步加载
不管 JavaScript 引擎是同步加载模块(如服务器),还是异步加载模块(如浏览器),ES6 模块都能独立工作。它的语法非常适合同步加载,另外由于它是静态结构的,因此可以静态确定所有的导入,就能够确保在执行主体前异步加载这些模块。
支持模块之间的循环依赖关系
如果 模块 A
import 模块 B
,模块 B
import 模块 A
,则这两个模块彼此循环依赖。可能的话,我们应该尽量避免循环依赖,这会导致 A 和 B 的紧密耦合。
但是有的情景下我们还是会用到循环依赖,比如一个 DOM tree,父节点需要指向子节点,子节点需要指向父节点,因此支持循环依赖是必要的。
CommonJS 中的循环依赖关系
//------ a.js ------
var b = require('b')
exports.foo = function () { ... }
//------ b.js ------
var a = require('a') // #1
// 无法在模块正文中使用 a.foo
exports.bar = function () {
a.foo(); // OK #2
}
//------ main.js ------
var a = require('a')
只有在模块 B 的解析执行完成后,foo 属性才能在 B 的 exports 中取得。
使用循环依赖,你无法在模块正文中访问到导入的内容,这是固有现象,不会随着 ES6 modules 的出现而改变。
CommonJS 方法的局限是:
Node.js 风格的单值导出将不起作用,在 Node.js 里,你可以导出个单值而不是必须导出对象。
module.exports = function () { ... }
如果在 A 中这么做,将无法在 B 中使用 A 输出的函数,因为 B 里的变量 a 仍然指向 A 原始输出的对象。
不能直接使用命名导出。也就是说模块 B 不能像这样导入 a.foo
var foo = require('a').foo // foo 将会得到 undefined
ES6 中的循环依赖关系
为了消除上述的两个局限,ES6 modules 输出引用而非值,也就是说,保持模块内部声明的变量的链接的有效性。因此在面对依赖关系,直接访问导出的命名或者是通过模块访问都无关紧要,无论在哪种情况下,都存在间接有效的方式。
Importing
ES6 Modules 提供以下几种 import 方式:
// Default exports and named exports
import theDefault, { named1, named2 } from 'src/mylib'
import theDefault from 'src/mylib'
import { named1, named2 } from 'src/mylib'
// Renaming: import named1 as myNamed1
import { named1 as myNamed1, named2 } from 'src/mylib'
// Importing the module as an object
// (with one property per named export)
import * as mylib from 'src/mylib'
// Only load the module, don't import anything
import 'src/mylib'
Exporting
ES6 Modules 有以下几种导出方式,一种是使用关键字声明导出。
export var myVar1 = ...
export let myVar2 = ...
export const MY_CONST = ...
export function myFunc() {
...
}
export function* myGeneratorFunc() {
...
}
export class MyClass {
...
}
也可以使用默认导出一个表达式
export default 123
export default function(x) {
return x
}
export default x => x
export default class {
constructor(x, y) {
this.x = x
this.y = y
}
}
另一方面,我们也可以在模块末尾列出想要导出的所有内容
const MY_CONST = ...
function myFunc() {
...
}
export { MY_CONST, myFunc }
也可以给输出的内容重命名
export { MY_CONST as THE_CONST, myFunc as theFunc }
重复输出
重新导出意味着将另一个模块的导出添加到当前模块的导出,可以将模块整个导出
export * from 'src/other_module'
或者可以选择性导出(也可以重命名)
export { foo, bar } from 'src/other_module'
// Export other_module's foo as myFoo
export { foo as myFoo, bar } from 'src/other_module'
export { foo as default } from 'src/other_module'