CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”而后者是“静态的”。
“动态”的含义是,模块依赖关系的建立发生在代码运行阶段;
而“静态”则是模块依赖关系的建立发生在代码编译阶段
先看一个CommonJS的例子
// calculator.js
module.exports = {name:'calculator'};
// index.js
const name = require(./calculator.js).name;
在前面提到过,require的模块路径可以动态指定,支持传人一个表达式,甚至可以通过if语句判断是否加载某个模块。因此,在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导人、导出发生在代码的运行阶段。
ES6 Module的导人、导出语句都是声明式的,它不支持导人的路径是一个表达式并且导人、导出语句必须位于模块的顶层作用域,因此我们说,ES 6Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系。
相比于CommonJS来说具备以下几点优势:
值拷贝与动态映射
在导人一个模块时,对于CommonJS来说获取的是一份导出值的拷贝,而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
要理解上面的概念,看下面的例子
CommonJS中的值拷贝。
// calculator.js
var count = 0
module.exports = {
count: count,
add: function(a,b){
count+=1
return a+b
}
}
// index.js
var count = require('./calculator.js').count
var add = require('./calculator.js').add
console.log(count); // 0 (这里的count是对 calculator,js 中count 值的持贝)
add(2,3);
console,log(count); // 0 (calculator.js中变量值的改变不会对这里的拷贝值造成影响)
count +=1;
console.log(count);// 1(拷贝的值可以更改)
index.js中的count是对 calculator.js 中count的一份值拷贝,因此在调用add函时,虽然更改了原本calculator.js中count的值,但是并不会对index.js 中导人时创建的副本造成影响。
另一方面,在CommonJS中允许对导人的值进行更改。我们可以在index.js更改count和add,将其赋予新值。同样,由于是值的拷贝,这些操作不会影响 calculator.js本身。
ES6 Module
// calculator.js
var count = 0
module.exports = {
count: count,
add: function(a,b){
count+=1
return a+b
}
}
export{count, add}
// index.js
import ( count,add ) from './calculator.js';
console.log(count);//0(对 calculator.js中 count 值的映射)
add(2,3);
console.log(count);//1 (实时反映calculator.js 中count值的变化)
// count +=1; //不可更改,会抛出SyntaxError:"count"is read-only
index.js中的count是对calculator.js中的count值的实时反映,当我们通过调用add 函数更改了calculator.js中count值时,index,js 中count的值也随之变化。
我们不可以对ES6 Module导人的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里我们可以实时观察到原有的事物,但是并不可以操纵镜子中的影像。
循环依赖
循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。比如下面这个例子:
//a.js
import ( foo )from './b,js';
foo();
// b.js
import ( bar ) from./a.js';
bar();
一般来说工程中应该尽量避免循环依赖的产生,因为从软件设计的角度来说,单向的依赖关系更加清晰,而循环依赖则会带来一定的复杂度。而在实际开发中,循环依赖有时会在不经意间产生,因为当工程的复杂度上升到足够规模时,就容易出现隐藏的循环依赖关系。
简单来说,A和B两个模块之间是否存在直接的循环依赖关系是很容易被发现的。但是当中间模块太多时就很难发现A和B之间存在着隐式的循环依赖。
那如何解决循环依赖的问题
其实可以利用ES6 Module的特性是其支持循环依赖,看下面的例子
// index.js
import foo from './foo.js'
foo('index.js')
// foo.js
import bar from './bar.js'
function foo(invoker){
console.log(invoker + ' invokes foo.js';
bar('foo.js');
}
export default foo;
// bar.js
import foo from './foo.js'
let invoker = false
function bar(invoker){
if(!invoker){
invoker = true;
console.log(invoker + ' invokes bar.js';
foo('bar.js');
}
}
export default bar;
上面代码的执行结果如下:
indexjs invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js
可以看到,foo.js和bar.js 这一对循环依赖的模块均获取到了正确的导出值。下面让我们分析一下代码的执行过程。
由上面的例子可以看出,ES6 Module的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导人的值被使用时已经设置好正确的导出值