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/exportrequire/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 创造了名为 counterincCounterconst 变量。

// 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'

results matching ""

    No results matching ""