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对象。
- 严格模式,关键字



