You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js CommonJS与ESModule循环依赖行为差异原因咨询

ESModule与CommonJS循环依赖处理差异原因

两者表现不一致核心是底层模块加载、导出机制的设计完全不同,不存在逻辑对错,只是规范选型的区别。

CommonJS的循环依赖处理逻辑

  • CommonJS是运行时同步加载规范,模块执行到require()语句时,才会同步加载、执行被依赖模块的代码。
  • 每个模块启动时会提前创建一个空的exports普通对象,模块执行过程中逐步往这个对象上挂载导出属性。
  • 遇到循环依赖时,直接返回当前状态的exports对象——哪怕模块本身代码还没执行完、对象上只挂载了部分属性,也就是你观察到的「未完成模块」。
    你的测试案例里,b.js反向加载a.js时,a.js刚执行完exports.done = false,后续逻辑还没运行,所以b拿到的就是已经挂了done:false的exports对象,不会触发报错。

ESModule的循环依赖处理逻辑

  • ESModule是编译时预解析规范,所有import语句会被提升到模块顶部,在任何业务代码执行前,就会先完成整个依赖图的解析、链接,建立导入和导出之间的实时绑定关系,不会等执行到import语句才去加载依赖。
  • ESM的导出绑定遵循和let/const一致的暂时性死区(TDZ)规则:在模块代码执行到导出变量的初始化语句之前,任何访问该绑定的行为都会直接抛出引用错误,不会像CommonJS那样提前返回一个可随意访问的半成品导出对象。

测试代码报错的具体原因

代码执行的实际流程如下:

  1. 预解析阶段完成三个模块的依赖链接,建立b.js中A_done和a.js导出的done、a.js中B_done和b.js导出的done的实时引用关系。
  2. 从入口开始执行,首先触发a.js模块执行:
    • 因为import被提升,a.js还没执行自身的变量声明逻辑,就先跳去执行b.js的代码
    • b.js执行第一行打印b starting,随后执行console.log('in b, a.done = %j', A_done)时,a.js里的export let done = false还没被执行到,done绑定还处于暂时性死区,直接抛出ReferenceError: Cannot access 'A_done' before initialization。

ESM是否会返回未完成的模块?

不会返回CommonJS那种可以任意访问属性的未完成导出对象:

  • 如果循环依赖场景下,你访问的导出绑定已经完成初始化,ESM可以正常给到你实时绑定的值(和CommonJS值拷贝的逻辑不同,绑定值会跟着导出模块的变量变化实时更新)。
  • 如果访问的绑定还没执行到初始化语句,会直接触发TDZ错误,不会静默返回半成品值。

如果要在ESM下复现你CommonJS测试用例的同等效果,需要保证循环依赖访问的变量在触发反向依赖前就完成初始化,比如把变量声明放到会触发其他模块加载的逻辑之前,或者用会被提升的函数声明作为导出值。


内容的提问来源于stack exchange,提问作者MINJA KIM

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 19:03:28