字数
1778 字
阅读时间
8 分钟
ES Modules (ESM) 与 CommonJS (CJS) 的区别、优劣及设计差异
一、核心区别
ES Modules(ES6 引入的官方模块系统)和 CommonJS(Node.js 采用的模块系统)是 JavaScript 中两种主流的模块化方案,核心区别如下:
| 维度 | ES Modules (ESM) | CommonJS (CJS) |
|---|---|---|
| 语法 | 使用 import 导入、export 导出 | 使用 require() 导入、module.exports 导出 |
| 加载时机 | 静态加载(编译时解析模块依赖) | 动态加载(运行时解析模块依赖) |
| 导入性质 | 导入的是绑定(binding)(只读引用) | 导入的是值的拷贝(原始类型拷贝,对象浅拷贝) |
| 模块标识 | 支持相对路径、绝对路径、URL(如 http://) | 支持相对路径、绝对路径、内置模块、npm 包 |
| 异步性 | 支持异步加载(如 import() 动态导入) | 同步加载(阻塞执行) |
顶层 this | undefined | 指向当前模块 module.exports |
| 循环依赖处理 | 基于“引用”,未执行的模块会返回空对象占位 | 基于“值拷贝”,未执行完的模块返回已执行部分 |
二、优劣对比
1. ES Modules (ESM)
优势:
- 静态分析支持:编译时即可确定模块依赖关系,支持 tree-shaking(消除未使用代码)、类型检查(如 TypeScript)、IDE 智能提示等优化。
- 只读引用:导入的变量是原模块的绑定,原模块更新后导入方会同步感知(更符合“模块”的设计理念)。
- 异步加载:支持
import()动态导入(返回 Promise),适合浏览器端按需加载(如路由懒加载)。 - 标准化:ECMAScript 官方标准,浏览器和 Node.js 均已支持(Node.js 需通过
.mjs或package.json "type": "module"启用)。
劣势:
- 兼容性限制:旧浏览器(如 IE)不支持,需通过 Babel 等工具转译。
- 静态语法限制:
import语句必须位于顶层(不能在条件语句中),模块路径不能是动态表达式(如import('./' + path),需用import()替代)。
2. CommonJS (CJS)
优势:
- 动态灵活性:
require()可以在任意位置调用(如条件语句、函数内),路径支持动态表达式(如require('./' + filename))。 - Node.js 生态默认支持:大量 npm 包基于 CJS 编写,无需额外配置即可运行。
- 同步加载简单直接:适合 Node.js 服务端场景(文件读取快,同步加载逻辑更直观)。
劣势:
- 不支持静态优化:运行时才能解析依赖,无法实现 tree-shaking,打包体积较大。
- 值拷贝问题:导入的是值的拷贝,原模块更新后导入方不会感知(对象类型虽为浅拷贝,但修改引用需手动重新导入)。
- 浏览器不原生支持:需通过 Webpack、Browserify 等工具打包后才能在浏览器运行。
三、设计理念:为何 ESM 静态化,CJS 动态化?
两种模块系统的设计差异源于其目标场景和需求的不同:
1. ESM 静态化的原因
ESM 诞生的核心目标是为 JavaScript 提供一个标准化、高效的模块系统,兼顾浏览器和服务器端,尤其优化浏览器端的加载和解析效率。
- 静态分析需求:浏览器端代码需要通过网络加载,静态化允许在编译时(甚至构建阶段)确定依赖关系,从而实现:
- 依赖预加载(提前请求所需模块,减少加载时间);
- tree-shaking(删除未使用的代码,减小包体积);
- 静态类型检查(如 TypeScript 依赖模块结构的静态确定)。
- 模块化规范性:静态语法(
import必须顶层声明)强制模块依赖关系清晰可见,避免动态加载导致的代码逻辑混乱,提升可维护性。 - 与 ES 语言特性协同:ESM 设计时结合了块级作用域、
let/const等新特性,静态绑定(只读引用)更符合变量的声明语义。
2. CJS 动态化的原因
CJS 是 Node.js 为了解决服务端模块化需求而设计的,核心目标是灵活处理文件系统中的模块加载。
- 服务端场景特性:Node.js 模块主要从本地文件系统加载,同步加载成本低,且需要应对复杂的动态场景:
- 动态路径加载(如根据配置文件动态加载不同模块);
- 条件加载(如根据环境变量
process.env.NODE_ENV加载开发/生产模块)。
- 快速迭代需求:Node.js 早期追求简单实用,动态化设计实现起来更直观,无需复杂的编译时分析,适合快速开发服务端工具和应用。
- CommonJS 规范历史:CJS 源于社区早期的模块化探索(如
require.js之前的方案),动态化是当时对“灵活性”的优先选择。
四、总结
- ESM 是标准化的静态模块系统,适合需要优化、Tree-shaking、跨平台(浏览器/Node.js)的场景,牺牲了部分动态灵活性换取性能和规范性。
- CJS 是动态模块系统,适合 Node.js 服务端场景,灵活性高但无法支持静态优化,主要依赖运行时解析。
随着 ESM 被 Node.js 逐步支持(自 v12 起稳定),生态正逐步向 ESM 迁移,但 CJS 因历史积累仍会长期存在,二者的互操作(如 ESM 导入 CJS 模块)也是当前的重要议题。
其他模块
- AMD 和 CMD 都是浏览器端的 JS 模块化规范,分别由 require.js 和 sea.js 实现
- CommonJS 是服务器端的 js 模块化规范,由 NodeJS 实现
- UMD 是希望解决跨平台的解决方案 (UMD 是 AMD 和 Common JS 的糅合)
AMD - requireJS
The Asynchronous Module Definition (AMD) 规范
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出
CMD - seaJS
同理
区别
1.CMD 推崇依赖就近,AMD 推崇依赖前置
2.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行
参考
Common JS、AMD、CMD 和 UMD 的区别 - 掘金
从 Nodejs 如何解决模块循环依赖问题来一点关于模块的发散思考 - 掘金