felix-cao / Blog

A little progress a day makes you a big success!
31 stars 4 forks source link

ES6模块(Module)和 CommonJS 模块加载的实质。 #49

Open felix-cao opened 6 years ago

felix-cao commented 6 years ago

《ES6 模块(Module)》 中介绍了 ES6 中提供了两个命令exportimport 来导出和导入模块,在 《CommonJS 的模块规范》 介绍了 CommonJS 的模块规范。那么 ES6CommonJS 两种模块的加载机制是怎样的呢?本文就来聊一聊这个话题:

ES6 CommonJS
输出语法 export module.exports,简写exports
加载语法 import require
输出的值 值的引用,动态的只读引用 值的拷贝,值缓存
加载时机 编译时加载 运行时加载
顶层this 指向这个模块本身 指向undefined

模块化和块级作用域使得曾经大为流行的立即执行函数已经成为鸡肋, 模块化也使得 javascript 这门面向对象语言的封装特性得到极大的加强。

一、CommonJS 模块输出的是被输出值的拷贝

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

// lib.js
var counter = 3;
function incCounter() {
 counter++;
}
module.exports = {
 counter: counter,
 incCounter: incCounter,
};

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

// 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 = {
  counter: function() {
  return counter
 },
 incCounter: incCounter,
};

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

二、ES6 模块是动态引用,并且不会缓存值

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,import输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

还是举上面的例子。

// 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模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。

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

// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError

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

// mod.js
function C() {
 this.sum = 0;
 this.add = function () {
  this.sum += 1;
 };
 this.show = function () {
  console.log(this.sum);
 };
}
export let c = new C();

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

// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';

现在执行main.js,输出的是1。这就证明了x.js和y.js加载的都是C的同一个实例。