esm是静态编译时解析、活绑定、拓扑执行;cjs是动态运行时加载、值拷贝、顺序执行:前者支持tree-shaking和浏览器原生运行,后者灵活支持条件引入但难以优化,二者互操作需动态import或配置转换。

核心差异不在语法表面,而在于模块系统背后的执行逻辑和设计哲学。ES 模块(ESM)是语言级的静态规范,CommonJS(CJS)是运行时的动态约定。
加载时机:编译时 vs 运行时
ESM 的 import 语句在代码解析阶段就被识别,依赖关系在执行前就已确定。这使得构建工具能做静态分析、tree-shaking、死代码消除。import 必须写在顶层,不能放在 if 或函数里(除非用 import() 动态导入)。
CommonJS 的 require 是普通函数调用,在代码真正执行到那一行时才去读文件、运行模块、返回结果。所以它可以写在任意位置,比如条件分支中:
- if (env === 'dev') require('./debug-tools')
- const mod = require(path + '/utils')
导出绑定:活引用 vs 值拷贝
ESM 导出的是“活绑定”(live binding)。如果一个模块导出了 let count = 0,另一个模块 import { count } 后,原模块内部修改了 count,导入方看到的值也会变。
CommonJS 导出的是值的快照。module.exports = { count } 相当于把当前 count 的值复制出去;后续原模块里 count 变了,require 拿到的对象里 count 不会自动更新。
- ESM:导出的是“指针”,指向同一内存位置
- CJS:导出的是“照片”,拍完就定格
执行顺序与循环引用
ESM 按图拓扑排序执行,所有模块先创建空的导出对象,再依次执行脚本。遇到循环引用时,能拿到对方已初始化的导出对象(哪怕还没执行完),不会报错但可能值为 undefined。
CommonJS 遇到循环引用时,会返回当前已执行部分的 module.exports 对象(可能是不完整的),更易出现未定义行为,调试起来更隐晦。
- ESM 循环引用:安全、可预测、返回空壳对象
- CJS 循环引用:依赖执行顺序,容易拿不到想要的属性
环境支持与互操作性
浏览器原生只支持 ESM(<script type="module"></script>),不支持 require。Node.js 从 v12 开始支持 ESM,但需满足以下任一条件:
- 文件扩展名为
.mjs - package.json 中设置
"type": "module" - 通过
node --input-type=module执行字符串代码
两者不能直接混用:不能在 ESM 文件里写 require,也不能在 CJS 文件里写 import(除非用转译或实验性标志)。跨系统调用需借助动态 import() 或构建工具桥接。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











