语法差别
CommonJS 模块使用require()
和module.exports
,ES6 模块使用import
和export
。
总结为一句话:.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require
命令不能加载.mjs
文件,会报错,只有import
命令才可以加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import
。
ES6 模块与 CommonJS 模块的差异
它们有三个重大差异。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。
第二个差异产生的原因是 CommonJS 导出一个对象,对象需要在脚本运行完成时才生成. ES6 模块的对外接口是一种静态定义,在代码静态解析阶段就会完成.
循环加载
“循环加载”(circular dependency)指的是,a
脚本的执行依赖b
脚本,而b
脚本的执行又依赖a
脚本。
CommonJS 模块加载原理
CommonJS 的一个模块就是一个文件. require
命令第一次时,就执行完整个脚本, 并在内存中生成一个对象
{
id: '...',
exports: { ... },
loaded: true,
...
}
这个对象的id
属性是模块名,export
属性是模块输出的全部接口,loaded
属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
以后需要用到这个模块的时候,就会到exports
属性上面取值。即使再次执行require
命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
CommonJS 模块的循环加载
a.js
:
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
:
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js
:
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
整体思路,
main.js
引入a.js
,a.js
运行到第三行代码- 进入到
b.js
- 当运行到
b.js
的第三行代码,b.js
知道是它是被a.js
引入,所以b.js
只会获取到a.js
前两行,但仅只有exports.done = false
生效。b.js
往下执行,第五行导出done = true
,最后结束。 - 返回到
a.js
的第三行,取到b.js 的 dong=true
, - 第五行导出
done = true
,最后输出。
ES6 模块的循环加载
ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import
从一个模块加载变量(即import foo from 'foo'
),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。