ESM采用实时绑定,导入的是导出值的动态引用,修改导出端立即反映在所有导入处;CommonJS导入的是执行时刻的值拷贝,后续变化不影响已导入值。

ESM 的“实时绑定”是指导入的变量始终与原始导出值保持动态关联,修改导出端会立即反映在所有导入处;而 CommonJS 导入的是模块 执行那一刻 的值拷贝,后续导出端变化不会影响已导入的值。
实时绑定:值是活的引用
ESM 中 import 声明不创建副本,而是建立对导出声明的只读绑定。只要导出的是顶层绑定(如 export let x = 1 或 export const f = () => {}),导入后对其的读取总能拿到最新值。
- 导出模块中修改
let/var/const声明的变量,导入方立刻可见 - 函数、类、对象等导出项本身不可重赋值(只读),但其内部属性或状态可变
- 即使模块尚未完全执行完毕,循环引用中也能安全访问未初始化的绑定(表现为
undefined,而非报错)
值拷贝:导入即快照
CommonJS 的 require() 返回的是模块 module.exports 对象的一个浅拷贝(实际是引用该对象,但导出字段本身是值拷贝)。关键点在于:它捕获的是 导出时那个时刻的值,不是持续连接。
- 基本类型(
number、string、boolean)导入后彻底独立,原模块改了不影响 - 对象/函数导入的是引用,所以修改对象属性或调用方法仍会影响原对象——但这不是“绑定”,只是 JS 引用行为本身
- 如果导出的是
exports.count = count这种赋值,count是原始值,导入后就和原变量断开
一个直观对比示例
// counter.mjs(ESM)
export let count = 0;<br>export function increment() { count++; }
// main.mjs
import { count, increment } from './counter.mjs';<br>console.log(count); // 0<br>increment();<br>console.log(count); // 1 ✅
// counter.cjs(CommonJS)
let count = 0;<br>module.exports = { count, increment: () => count++ };
// main.cjs
const { count, increment } = require('./counter.cjs');<br>console.log(count); // 0<br>increment();<br>console.log(count); // 0 ❌(仍是初始拷贝)
为什么这个区别重要
实时绑定让 ESM 更适合构建响应式系统、共享状态模块或需要强一致性的工具链(比如配置中心、计数器服务);而 CommonJS 的拷贝机制更符合直觉,也更适合封装“一次性初始化”的逻辑。两者没有绝对优劣,但在跨模块通信、热更新、测试模拟等场景下,行为差异会直接影响代码是否按预期运行。











