前言
CommonJS
是由 JavaScript
社区于 2009 年提出的包含模块、文件、IO、控制台在内的一系列标准。在Node.js
的实现中采用了 CommonJS
标准的一部分,并在其基础上进行了一些调整。我们所说的CommonJS
模块和 Node.js
中的实现并不完全一样,现在一般谈到 CommonJS
其实是 Node.js
中的版本,而非它的原始定义。
CommonJS
最初只为服务端而设计,直到有了 Browserify
——一个运行在 Node.js
环境下的模块打包工具,它可以将 CommonJS
模块打包为浏览器可以运行的单个文件。这意味着客户端的代码也可以遵循CommonJS
标准来编写了。不仅如此,借助 Node.js
的包管理器,npm
开发者还可以获取他人的代码库,或者把自己的代码发布上去供他人使用。这种可共享的传播方式使 CommonJS
在前端开发中逐渐流行了起来。
CommonJS
中规定每个文件是一个模块。将一个 JavaScript
文件直接通过 script
标签插入页面中与封装成CommonJS
模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。请看下面的例子:
// calculator.js
var name = 'calculator.js';
// index.js
var name = 'index.js';
require('./calculator.js');
console.log(name); // index.js
这里有两个文件,在 index.js
中我们通过 CommonJS
的 require
函数加载 calculator.js
。运行之后控制台结果是 “index.js”
,这说明 calculator.js
中的变量声明并不会影响 index.js
,可见每个模块是拥有各自的作用域的。
CommonJs 与 ES6 Module 区别
一、动态与静态
CommonJS
与 ES6 Module
最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。
ES6
代码的编译阶段就可以分析出模块的依赖关系。它相比于 CommonJS
来说具备以下几点优势:
-
死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
-
模块变量类型检查。
JavaScript
属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module
的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。 -
编译器优化。在
CommonJS
等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module
支持直接导入变量,减少了引用层级,程序效率更高。
二、拷贝与映射
CommonJS
index.js
中的 count
是对 calculator.js
中 count
的一份值拷贝,因此在调用 add
函数时,虽然更改了原本 calculator.js
中 count
的值,但是并不会对 index.js
中导入时创建的副本造成影响。另一方面,在CommonJS
中允许对导入的值进行更改。我们可以在 index.js
更改 count
和 add
,将其赋予新值。同样,由于是值的拷贝,这些操作不会影响 calculator.js
本身。
// 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(拷贝的值可以更改)
ES6 Module
ES6 Module
中导入的变量其实是对原有值的动态映射。index.js
中的 count
是对 calculator.js
中的count
值的实时反映,当我们通过调用 add
函数更改了 calculator.js
中 count
值时,index.js
中 count
的值也随之变化。我们不可以对 ES6 Module
导入的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里我们可以实时观察到原有的事物,但是并不可以操纵镜子中的影像。
// calculator.js
let count = 0;
const 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
三、循环依赖
循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。比如下面这个例子:
// a.js
import { foo } from './b.js';
foo();
// b.js
import { bar } from './a.js';
bar();
CommonJS
// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports = 'This is foo.js';
// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports = 'This is bar.js';
// index.js
require('./foo.js');
在这里,index.js
是执行入口,它加载了 foo.js
,foo.js
和 bar.js
之间存在循环依赖。让我们观察foo.js
和 bar.js
中的代码,理想状态下我们希望二者都能导入正确的值,并在控制台上输出。
value of foo: This is foo.js
value of bar: This is bar.js
而当我们运行上面的代码时,实际输出却是:
value of foo: {}
value of bar: This is bar.js
为什么 foo
的值会是一个空对象呢?让我们从头梳理一下代码的实际执行顺序。
- 1)
index.js
导入了foo.js
,此时开始执行foo.js
中的代码。 - 2)
foo.js
的第1句导入了bar.js
,这时foo.js
不会继续向下执行,而是进入了bar.js
内部。 - 3)在
bar.js
中又对foo.js
进行了require
,这里产生了循环依赖。需要注意的是,执行权并不会再交回foo.js
,而是直接取其导出值,也就是module.exports
。但由于foo.js
未执行完毕,导出值在这时为默认的空对象,因此当bar.js
执行到打印语句时,我们看到控制台中的value of foo
就是一个空对象。 - 4)
bar.js
执行完毕,将执行权交回foo.js
。 - 5)
foo.js
从require
语句继续向下执行,在控制台打印出value of bar
(这个值是正确的),整个流程结束。由上面可以看出,尽管循环依赖的模块均被执行了,但模块导入的值并不是我们想要的。因此在CommonJS
中,若遇到循环依赖我们没有办法得到预想中的结果。
ES6 Module
// foo.js
import bar from './bar.js';
console.log('value of bar:', bar);
export default 'This is foo.js';
// bar.js
import foo from './foo.js';
console.log('value of foo:', foo);
export default 'This is bar.js';
// index.js
import foo from './foo.js';
执行结果如下:
// value of foo: undefined
// foo.js:3 value of bar: This is bar.js
很遗憾,在 bar.js
中同样无法得到 foo.js
正确的导出值,只不过和 CommonJS
默认导出一个空对象不同,这里获取到的是 undefined
。
上面我们谈到,在导入一个模块时,CommonJS
获取到的是值的拷贝,ES6 Module
则是动态映射,那么我们能否利用 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 invoked = false;
function bar(invoker) {
if(!invoked) {
invoked = true;
console.log(invoker + ' invokes bar.js');
foo('bar.js');
}
}
export default bar;
执行结果如下
// index.js invokes foo.js
// foo.js invokes bar.js
// bar.js invokes foo.js
可以看到,foo.js
和 bar.js
这一对循环依赖的模块均获取到了正确的导出值。下面让我们分析一下代码的执行过程。
- 1)
index.js
作为入口导入了foo.js
,此时开始执行foo.js
中的代码。 - 2)从
foo.js
导入了bar.js
,执行权交给bar.js
。 - 3)在
bar.js
中一直执行到其结束,完成bar
函数的定义。注意,此时由于foo.js
还没执行完,foo
的值现在仍然是undefined
。 - 4)执行权回到
foo.js
继续执行直到其结束,完成foo
函数的定义。由于ES6 Module
动态映射的特性,此时在bar.js
中foo
的值已经从undefined
成为了我们定义的函数,这是与CommonJS
在解决循环依赖时的本质区别,CommonJS
中导入的是值的拷贝,不会随着被夹在模块中原有值的变化而变化。 - 5)执行权回到
index.js
并调用foo
函数,此时会依次执行foo→bar→foo
,并在控制台打出正确的值。由上面的例子可以看出,ES6 Module
的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。
网友评论