本质区别在于设计哲学与执行模型:esm是为浏览器原生运行、编译时静态分析的模块系统,支持tree-shaking和活绑定;cjs是为node.js服务端定制的运行时动态加载机制,采用同步执行与值拷贝。

ES Modules(ESM)和 CommonJS(CJS)的本质区别,不在于“写法不同”,而在于设计哲学与执行模型的根本分歧:一个是为浏览器原生运行、编译时可分析的静态模块系统;另一个是为 Node.js 服务端环境定制、运行时动态执行的模块加载机制。
加载时机与解析方式
ESM 是静态的——import/export 必须出现在顶层作用域,JavaScript 引擎或构建工具在代码执行前就能完整识别所有依赖关系。这使得 Tree Shaking、循环依赖安全处理、编译期优化成为可能。
CJS 是动态的——require() 可以写在 if、for、函数内部,模块加载发生在代码实际运行到那一行时。Node.js 会同步读取文件、包裹执行、缓存 module.exports 结果,整个过程不可被静态工具提前推断。
- ESM 中
if (flag) import('./a.js')是合法的动态导入(返回 Promise),但if (flag) import './a.js'会报语法错误 - CJS 中
if (flag) const m = require('./a.js')完全合法,且每次调用都可能触发新加载(除非命中缓存)
值的绑定机制
ESM 导出的是实时绑定(live binding):导出的变量名与原始值保持引用关系。一个模块修改了 export let count = 0,另一个模块中 import { count } 立即看到更新。
CJS 导出的是执行快照(snapshot):module.exports 是模块执行完毕后的一次性赋值结果。后续对原始变量的修改,不会影响已通过 require() 获取的对象内容。
- 导出对象时,CJS 的
exports.obj = {...}和 ESM 的export const obj = {...}表面相似,但前者导出的是该时刻的副本引用,后者导出的是持续响应变化的绑定 - 若 CJS 模块中重赋值
module.exports = newObject,已require过的老引用仍指向旧对象
运行环境与加载行为
ESM 天然适配浏览器:<script type="module"></script> 支持原生异步加载,模块脚本默认 defer,支持跨域、CORS、完整性校验等 Web 标准能力。在 Node.js 中,ESM 也以异步方式解析并实例化模块图。
CJS 是 Node.js 的历史默认规范:同步阻塞式加载,依赖本地文件系统快速读取。它无法直接在浏览器中运行,必须经打包工具(如 Webpack)转译或封装。
- Node.js 中同时支持两种模块系统,但
.mjs文件强制 ESM,.cjs强制 CJS,.js则由package.json的"type": "module"决定 - ESM 在浏览器中支持
import.meta.url获取当前模块 URL;CJS 中对应的是__filename和__dirname(Node.js 特有)
语法约束与执行上下文
ESM 自动启用严格模式,禁止 arguments、with、删除不可配置属性等非严格操作;所有模块都是独立作用域,顶层 this 为 undefined。
CJS 模块默认非严格模式(除非手动加 "use strict"),顶层 this 指向当前模块的 exports 对象,允许自由变量声明(如漏写 var/let/const)。
- ESM 不允许混合
import和require在同一文件中(语法层面禁止);CJS 中可以requireESM 模块(需 Node.js ≥14 且启用实验性标志,但仅限默认导出) - ESM 的
export不是语句而是声明,不能出现在块级作用域;CJS 的module.exports是普通赋值,位置完全自由











