ES6模块化之前,模块加载方案主要有CommonJS、AMD、CMD 三种。CommonJS用于服务器,AMD、CMD用于浏览器。
ES6 模块的设计思想是尽量的__静态化__,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS模块
CommonJS 模块就是对象,输入时必须查找对象属性。
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
module.exports
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量
// lib.js
module.exports.func = function(){
console.log('This is module');
}
//main.js
let a = require('./lib');
a.func();//打印'This is module'
exports
Node为每个模块提供一个exports变量,指向module.exports。相当在每个模块头部,有一行这样的命令。
let exports = module.exports;
// lib.js
const func = function(){
console.log('This is module');
}
exports.func = func
//等于 module.exports.func = func
AMD
AMD规范是非同步加载模块,允许指定回调函数。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
require([module], callback);
- 第一个参数[module],是一个数组,里面的成员就是要加载的模块
- 第二个参数callback,则是加载成功之后的回调函数
//a.js
define(function(){
function foo(){
console.log('hello world!');
}
return {
foo
}
})
//main.js
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.foo()
b.foo()
})
CMD
- AMD是依赖关系前置,在定义模块的时候就要声明其依赖的模块
- CMD是按需加载依赖就近,只有在用到某个模块的时候再去require
define(function(require, exports, module) {
let a = require('./a')
a.foo()
// ...
let b = require('./b') //依赖可以就近书写
b.foo()
// ...
})
ES6 模块
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象
ES6 模块与 CommonJS 模块的差异
-
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
-
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。(CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。)
// lib.js
let counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: 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
let counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了
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
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz。表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
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();
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
//输出1