CommonJS处理循环加载的核心是提前暴露模块对象与状态缓存。1.首次require时即创建空exports对象并缓存;2.b.js再次require('./a.js')直接返回该引用,即使a.js尚未执行完毕;3.读取的是a已写入的部分值(如done: false),后续赋值不影响已读内容;4.不阻断执行但可能导致undefined或旧值,依赖开发者保障一致性。

CommonJS 处理循环加载的核心是提前暴露模块对象 + 状态缓存,不阻断执行,但可能返回未完全初始化的值。
模块加载时就创建空 exports 对象
当 require 第一次加载某个模块(比如 a.js)时,Node 会立即在缓存(Module._cache)中创建一个模块实例,其中 exports 是一个空对象。这个对象从那一刻起就被“提前暴露”——后续无论谁 require 它,拿到的都是同一个引用,哪怕模块代码还没执行完。
- 这意味着 b.js 中
require('./a.js')不会重新执行 a.js,而是直接取缓存里的a.exports - 此时 a.js 可能只执行了前几行(比如
exports.done = false),后面导出的函数或变量还没赋值
执行顺序决定能读到什么值
CommonJS 按深度优先、同步顺序执行模块脚本。循环发生时,谁先开始执行,谁就“占先机”;后被 require 的一方只能读到前者已写入 exports 的部分。
- 例如:a.js 先执行 → 遇到
require('./b.js')→ 暂停,跳转执行 b.js - b.js 执行时再
require('./a.js')→ 拿到 a 的缓存对象 → 读到的是 a 当前已设置的字段(如done: false),不是最终值 - 等 b.js 执行完,a.js 才继续往下走,把
exports.done = true补上
不会报错,但结果可能不符合预期
CommonJS 不禁止循环依赖,也不做静态检查,它靠运行时机制“硬扛”过去。这带来两个典型表现:
立即学习“Java免费学习笔记(深入)”;
-
值拷贝陷阱:如果 a.js 导出的是基本类型(如
exports.flag = false),b.js 读到的是快照值,后续 a 修改它不影响 b 已读取的内容 -
函数/对象可访问:如果 a.js 导出的是函数声明或对象属性,只要在 require 前已定义(如
exports.fn = () => {}),b.js 就能调用——因为函数对象本身已被挂到 exports 上
本质是“执行中导出”,而非“执行完才导出”
CommonJS 模块没有独立的链接阶段,导出和执行交织在一起。它的循环处理逻辑不是“解决依赖”,而是允许模块在未就绪状态下被引用,把一致性责任交给开发者。
- 好处:简单、兼容性强,避免崩溃
- 风险:容易产生竞态逻辑,比如读到
undefined或旧值,调试困难


















