ES6 模块与 CommonJS 模块的差异

讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。

它们有两个重大差异。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

下面重点解释第一个差异。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。

  1. // lib.js
  2. var counter = 3;
  3. function incCounter() {
  4. counter++;
  5. }
  6. module.exports = {
  7. counter: counter,
  8. incCounter: incCounter,
  9. };

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

  1. // main.js
  2. var mod = require('./lib');
  3. console.log(mod.counter); // 3
  4. mod.incCounter();
  5. console.log(mod.counter); // 3

上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

  1. // lib.js
  2. var counter = 3;
  3. function incCounter() {
  4. counter++;
  5. }
  6. module.exports = {
  7. get counter() {
  8. return counter
  9. },
  10. incCounter: incCounter,
  11. };

上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。

  1. $ node main.js
  2. 3
  3. 4

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

还是举上面的例子。

  1. // lib.js
  2. export let counter = 3;
  3. export function incCounter() {
  4. counter++;
  5. }
  6. // main.js
  7. import { counter, incCounter } from './lib';
  8. console.log(counter); // 3
  9. incCounter();
  10. console.log(counter); // 4

上面代码说明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。

再举一个出现在export一节中的例子。

  1. // m1.js
  2. export var foo = 'bar';
  3. setTimeout(() => foo = 'baz', 500);
  4. // m2.js
  5. import {foo} from './m1.js';
  6. console.log(foo);
  7. setTimeout(() => console.log(foo), 500);

上面代码中,m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz

让我们看看,m2.js能否正确读取这个变化。

  1. $ babel-node m2.js
  2. bar
  3. baz

上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。

  1. // lib.js
  2. export let obj = {};
  3. // main.js
  4. import { obj } from './lib';
  5. obj.prop = 123; // OK
  6. obj = {}; // TypeError

上面代码中,main.jslib.js输入变量obj,可以对obj添加属性,但是重新赋值就会报错。因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为objconst变量。

最后,export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。

  1. // mod.js
  2. function C() {
  3. this.sum = 0;
  4. this.add = function () {
  5. this.sum += 1;
  6. };
  7. this.show = function () {
  8. console.log(this.sum);
  9. };
  10. }
  11. export let c = new C();

上面的脚本mod.js,输出的是一个C的实例。不同的脚本加载这个模块,得到的都是同一个实例。

  1. // x.js
  2. import {c} from './mod';
  3. c.add();
  4. // y.js
  5. import {c} from './mod';
  6. c.show();
  7. // main.js
  8. import './x';
  9. import './y';

现在执行main.js,输出的是1

  1. $ babel-node main.js
  2. 1

这就证明了x.jsy.js加载的都是C的同一个实例。