CommonJS
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
var math = require('math');
math.add(2,3); // 5
CommonJS的特点
- 所有代码都运行在模块作用域,不会污染全局作用域;
- 模块是同步加载的,即只有加载完成,才能执行后面的操作;
- 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;
- CommonJS输出是值的拷贝(即,
require
返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。
AMD
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
// a.js
//define可以传入三个参数,分别是字符串-模块名、数组-依赖模块、函数-回调函数
define(function(){
return 1;
})
// b.js
//数组中声明需要加载的模块,可以是模块名、js文件路径
require(['a'], function(a){
console.log(a);// 1
});
对于依赖的模块,AMD推崇依赖前置,提前执行。也就是说,在define
方法里传入的依赖模块(数组),会在一开始就下载并执行。
CMD
CMD推崇依赖就近,延迟执行。可以把你的依赖写进代码的任意一行,如下:
define(function(require, exports, module) {
var a = require('./a');
a.doSomething();
var b = require('./b');
b.doSomething();
})
这个规范实际上是为了Seajs的推广然后搞出来的。那么看看SeaJS是怎么回事儿吧,基本就是知道这个规范了。
同样Seajs也是预加载依赖js跟AMD的规范在预加载这一点上是相同的,明显不同的地方是调用,和声明依赖的地方。AMD和CMD都是用difine和require,但是CMD标准倾向于在使用过程中提出依赖,就是不管代码写到哪突然发现需要依赖另一个模块,那就在当前代码用require引入就可以了,规范会帮你搞定预加载,你随便写就可以了。但是AMD标准让你必须提前在头部依赖参数部分写好(没有写好? 倒回去写好咯)。这就是最明显的区别。
ES6
ES6 Module是ES6中规定的模块体系,相比上面提到的规范, ES6 Module有更多的优势,有望成为浏览器和服务器通用的模块解决方案。
Es6通过import
、export
实现模块的输入输出。其中import命令用于输入其他模块提供的功能,export命令用于规定模块的对外接口。
// a.js
export const a = 1
export const fn = () => 2
// b.js
export default const fn2 = (value) => value
// index.js
import { a, fn } from './a.js'
import fn2 from './b.js'
console.log(a) // 1
console.log(fn()) // 2
console.log(fn1(10)) // 10
ES6 Module的特点(对比CommonJS)
- CommonJS模块是运行时加载,ES6 Module是编译时输出接口;
- CommonJS加载的是整个模块,将所有的接口全部加载进来,ES6 Module可以单独加载其中的某个接口;
- CommonJS输出是值的拷贝,ES6 Module输出的是值的引用,被输出模块的内部的改变会影响引用的改变;
- CommonJS
this
指向当前模块,ES6 Modulethis
指向undefined
;
怎么理解上面的运行时加载/编译时输出接口、值的拷贝/值的引用?
commonJS例子
//calculator.js
var count=0;
module.exports={
count:count,
ADD:function (a,b) {
count+=1;
return a+b;
}
};
//inde.js
var count=require('./calculator').count;
var add=require('./calculator').ADD;
console.log(count); // 0
add(2,3);
console.log(count); // 0
count +=1;
console.log(count); // 1
index.js中的count是对calculator.js中的count的一份拷贝,因此在调用ADD函数的时候,虽然改变了calculator.js中的count的值,但是并不会对index.js中导入时创建的副本造成影响。也就是说你在index.js中只要不是通过ADD函数对count的操作,都不会影响到calculator.js中的count。而是影响了副本的count。
require的模块路径可以动态指定,并支持传入一个表达式或者一个if进行判断是否加载模块。因此可以看出,在commonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。
ES6 Module例子
//calculator.js
var count=0;
const ADD=function(a,b){
count+=1;
return a+b;
};
export {count,ADD}
//inde.html
<script type="module">
import {count,ADD} from './src/calculator.js';
console.log(count); // 0
ADD(2,3);
console.log(count); // 1
</script>
可以发现导入的变量其实是对原有值的动态映射。index.html中的count是对calculator.js中count值的实时反映,当我们调用ADD函数更改了calculator.js中count值时,index.html中的count的值也会随之变化。它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域不能放到if语句中。因此ES6 Module是一个静态模块结构。
循环加载
“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现这种现象。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目中很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
对于JavaScript语言来说,目前最常见的两种模块格式CommonJS和ES6在处理“循环加载”时的方法是不一样的,返回的结果也不一样。
CommonJS的一个模块就是一个脚本文件。require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象。
ES6模块是动态引用,如果使用import从一个模块中加载变量(即import foo from ‘foo’),那么,变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者保证在真正取值的时候能够取到值。
一个简单的例子:
// a.js
import {bar} from './b.js';
export function foo() {
console.log('foo');
bar();
console.log('执行完毕');
}
foo();
// b.js
import {foo} from './a.js';
export function bar() {
console.log('bar');
if (Math.random() > 0.5) {
foo();
}
}
按照CommonJS规范,上面的代码是无法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。
但是,ES6可以执行上面的代码。
$ babel-node a.js
foo
bar
执行完毕// 执行结果也有可能是
foo
bar
foo
bar
执行完毕
执行完毕
上面的代码中,a.js之所以能够执行,原因就在于ES6加载的变量都是动态引用其所在模块的。只要引用存在,代码就能执行。