ESM 和 CJS 的区别

前言
JavaScript
中的ESM(ES Modules)
规范和CJS(CommonJS)
规范是用于模块化开发的两种不同的模组系统。
早期 Javascript
这门语言是没有模块化的概念的,直到 Nodejs
诞生,才把模块系统引入 js``。Nodejs
已经在逐步支持 ESM
,目前很多主流浏览器也已经原生支持 ESM
。
关于 CSJ 和 ESM
关于 CJS
CJS
是 CommonJS
的模块系统,最初是为了在服务端 Nodejs
开发设计的,但也被广泛用于前端开发。CSJ
使用 require
函数来导入模块,并使用 module.exports
对象来导出模块。
1 | // math.js 定义模块 |
关于 ESM
ESM
是 ECMAScript
的模块系統,从 ECMAScript 6(ES6)开始引入并成为 JavaScript 的一部分。ESM 使用 import 和 export 关键字來定义和导入模块。
1 | // math.js 定义模块 |
模块入口
我们知道有很多第三方库同时支持在 Nodejs
和浏览器环境执行,这种库通常会打包出 CJS
和 ESM
两种产物,CJS
产物给 Nodejs
用,ESM
产物给 Webpack
之类的 Bundler
使用。所以,当我们使用 require
和 import
导入模块时,入口文件路径往往是不一样的。
那么问题来了,如何让 Nodejs
或者 Bundler
找到对应的入口文件呢?
一般我们通过 package.json
的 main
字段定义 CJS
的入口文件,module
字段定义 ESM
的入口文件。如下:
1 | { |
这样,Nodejs
和 Bundler
就知道分别从 ./dist/cjs/index.js
和 ./dist/esm/index.js
导入模块了。
区别
非严格模式 vs 严格模式
CJS
默认是非严格模式,而 ESM 默认是严格模式。
CommonJS
模块是在 Node.js
中早期的模块化系统,最初设计时并没有考虑到 JavaScript
的严格模式(strict mode
)。因此,CommonJS
模块的默认行为允许一些灵活的 JavaScript
特性。
ESM
模块是由 ECMAScript
规范引入的,是现代 JavaScript 的官方模块系统。严格模式(strict mode
)是 ECMAScript 5
引入的,旨在消除 JavaScript 中的一些不良行为,并提高代码的安全性和可维护性。
CJS(CommonJS)
:默认非严格模式- 允许在函数中使用未声明的变量。
- 可以在同一作用域中使用
this
来引用全局对象(例如global
)。 - 允许删除变量、对象的属性等,这些在严格模式中是禁止的。
ESM(ECMAScript Modules)
:默认严格模式- 严格模式下禁止使用一些危险的语言特性,例如禁止使用
with
语句,变量必须声明后才能使用,this
在函数中不会指向全局对象等。 ESM
是从ECMAScript 6(ES6)
开始设计的,目标是统一浏览器和Node.js
环境中的模块标准。因此,ESM
模块在设计时就默认启用了严格模式,目的是提升代码质量和减少潜在的错误。
- 严格模式下禁止使用一些危险的语言特性,例如禁止使用
拷贝 vs 引用
CJS
导入的是值的拷贝,而 ESM
导入的是值的引用。
CJS(CommonJS)
: 值的拷贝
1 | // math.js |
ESM(ECMAScript Modules)
: 值的引用
1 | // math.js |
可以看到,main.js
文件引用了 math.js
, 由于 ESM
是对值的引用,通过 setAge
修改 age
的值时,将影响到 math.js
模块中的内容,而 CJS
则不会。
动态 vs 静态
CJS(CommonJS)
: 动态加载
CJS
使用 require()
来加载模块,这个过程是运行时
(CJS 输出的是一个对象,该对象需要在脚本运行完成后才生成)动态解析的。当你调用 require()
时,Node.js
会在运行时查找、加载和解析模块,并返回该模块的内容。由于 require()
可能在代码的任何地方调用,因此无法在编译时就确定所有的依赖关系。
ESM(ECMAScript Modules)
: 静态加载
ESM
使用 import
和 export
语法,这些语法是显式的,且必须在文件的顶部进行声明。这使得 JavaScript
引擎可以在编译时
(即代码执行之前)就分析出所有的模块依赖关系。通过这种方式,ESM
可以在编译时进行优化(例如 tree shaking
,去除未使用的代码)和循环依赖检测。
总结:基于这个差异,ESM
比 CJS
好做 tree-shaking
。
注意:ECMAScript 2020(ES11)
已经引入 import()
函数,支持动态加载方案。import()
函数接受要加载的模块相对路径,返回一个 Promise
对象,内容是要加载的模块对象。
1 | import('./esm_module.js') |
同步 vs 异步
ESM(ES 模块)
和 CJS(CommonJS)
模块在加载机制上有一个根本的区别:ESM
是 静态异步加载,而 CJS
是 动态同步加载。我们可以通过分析它们的不同加载方式和特点来理解这一点。
CJS(CommonJS)
: 同步加载动态加载:在
CJS
中,require()
是可以放在任意位置的,JavaScript
引擎只有在执行到require()
语句时才会加载模块的内容。这意味着CJS
的模块是动态加载的,每次require()
都会去获取所需模块。同步阻塞:
require()
是同步的,模块的加载顺序依赖于代码的执行顺序。在加载过程中,模块的执行是同步的,并且是对值的拷贝,即在require()
会阻塞后续代码执行。
ESM(ECMAScript Modules)
: 异步加载模块解析和依赖关系的预分析:在代码运行之前,
JavaScript
引擎会先静态分析模块的依赖关系,将所有import
语句提前解析,以确保在代码开始执行时,所有模块和依赖已经加载好。这使得 ESM 可以在不阻塞主线程的情况下并行加载依赖。Top-Level Await 支持:
ESM
支持顶层await
,这使得模块加载可以等待异步操作完成。这样,在加载ESM
时,如果模块中有await
操作,整个模块会异步加载并等待这些操作完成后再继续执行。
总结
CJS(CommonJS)
- 非严格模式,关键字
require、module.exports
- 返回的是值的拷贝,可以重新赋值
- 动态加载:
运行时
查找、加载和解析模块 - 同步加载:require 函数会阻塞后续代码执行
- 非严格模式,关键字
ESM(ECMAScript Modules)
- 严格模式,关键字
import、export
- 值的引用,不可以重新赋值,但是如果是对象,可以修改对象内的属性,会影响到全局
- 静态加载:使用
import
和export
语法,这些语法是显式的,且必须在文件的顶部进行声明。使得js
引擎 可以在编译时
(即代码执行之前)就分析出所有的模块依赖关系。ECMAScript 2020(ES11)
已经引入import()
函数,支持动态加载方案。 - 异步加载: 支持顶层
await
设计,模块加载可以等待异步操作完成,返回的是promise
对象。
- 严格模式,关键字