以下内容均总结自es-modules-a-cartoon-deep-dive
1. ESM是异步的而CommonJS是同步的
异步是ESM面临的最大挑战。和CommonJS的执行环境Node获取模块文件的方式不同,ESM获取模块文件是通过网络。如果ESM从获取到执行所有模块都是顺序进行会导致主线程长期处于pending状态,无法响应用户事件。
import "https://example.com/main.js"
console.log("我在等上面导入的模块执行完成")
为了解决这个问题ESM将模块的解析和执行分成了三部分,分别是构建(Construction),实例化(Instantiation)和执行(Evaluation)。
浏览器在构建阶段会通过入口模块(<script type="module" src="https://example.com/main.js" />
)下载所有依赖的模块生成模块的依赖图。这部分工作不会阻塞主线程,此时主线程可以响应用户的点击滚动等事件。
而CommonJS则无此顾虑。Node脚本运行在服务器中,而对应的模块文件也可以在服务器的磁盘中获取而无需通过网络,所以模块加载和执行可以同步进行,没有加载时间过长阻塞主线程的顾虑。
2. ESM是编译时解析依赖而CommonJS是运行时解析依赖
一个字符串"https://example.com/main.js"
在编译时可以被理解为字符串,在运行时也是一个字符串,但是如果是一个标识符pathname
在编译时和运行时含义则打不相同,该标识符在代码没有执行到这块的时无法确定其具体值。这就导致ESM的module specifier(就是下图部分,图片来自es-modules-a-cartoon-deep-dive),无法使用标识符。
例:
import {count} from "./counter.js" // ok
import {count} from `${pathname}.js` // error
在CommonJS中module specifier中存在变量是合法的,因为CommonJS中模块的解析是在运行时,在运行时标识符pathname可以计算出值。
require(`${pathname}.js`) // ok
3. ESM导出的是引用而CommonJS导出的是值拷贝
在ESM中导入的变量会跟着导出的变动而变动。
ESM
// module.js
export let a = 1;
export function changeA() {a = 10;}
// entry.js
import {a, changeA} from './module.js';
changeA();
console.log(a); // 10
变量a
在原模块中变成10
后,在导入模块entry中也会跟着变化,所以在ESM中模块导出的是引用,具体实现可参考es-modules-a-cartoon-deep-dive。
CommonJS
// module.js
let a = 1;
const changeA = function() {a = 10;}
exports.a = a;
exports.changeA = changeA;
// entry.js
const module = require('./module.js');
module.changeA()
console.log(a) // 1
变量在元模块中变成10
之后导入模块entry中并没有跟着变化,所以在ESM中模块导出的是值。
补充
可以将JS中的变量简单理解为一个盒子,这个盒子中可以存放数字,字符串等。
const a = 1;
const b = a;
上例中声明了一个变量a
对应一个盒子,这个盒子里面放的是数字1
,然后又声明了一个盒子b
,这个b
中放的是从盒子a
中拿出来的值1
。所以当盒子a
中的值从1
变成2
的时候b
盒子中的1
并不会跟着变化。
JS的变量对应的盒子中不止能放值,还能放一个盒子。
const a = {value: 1};
const b = a;
上例中声明了一个a盒子,里面放了一个盒子{value: 1}
,然后又声明了一个盒子b,里面放的也是盒子{value: 1}
。这时当盒子{value: 1}
的value
值从1
变成2
,a
和b
盒子都能取到最新值。
所以ESM导出的是这个盒子,而CommonJS导出的是值。具体实现可参考es-modules-a-cartoon-deep-dive。
4. ESM执行在严格模式下而CommonJS未自动开启严格模式
在ESM下从入口开始所有脚本都默认按照严格模式解析,其影响包括但不限于:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
- eval不会在它的外层作用域引入变量
- eval和arguments不能被重新赋值
- arguments不会自动反映函数参数的变化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局对象
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈
- 增加了保留字(比如protected、static和interface)
以上限制信息来自于Module 的语法
对于JS模块文件的识别是通过<script type="module" src="https://example.com/main.js" />
入口文件对应标签上的属性type="module"
识别的,被改文件引用的所有文件都会被当做模块文件解析。
而在CommonJS中因为没有script标签作为入口明确识别模块文件,所以时间变的些许复杂,查阅相关资料显示可以通过.mjs
后缀名来让Node明白这是一个ESM文件,可以启用严格模式。
参考
- es-modules-a-cartoon-deep-dive
- Module 的语法