commonjs循环引用返回未执行完的模块对象,导致访问未初始化属性报错;es6模块则返回实时绑定,避免此类问题。

CommonJS 在遇到循环引用时不会直接报 ReferenceError,而是返回一个**尚未执行完毕的模块对象(空壳)**,导致部分导出值为 undefined。这不是语法错误,但后续调用未初始化的属性或方法时,就会触发 Cannot read property 'xxx' of undefined 或 xxx is not a function 这类运行时报错。
CommonJS 循环引用的实际执行过程
Node.js 的 require 是同步、按需加载的。当模块 A require 模块 B,而 B 又 require A 时,A 的执行会被暂停,B 先拿到 A 当前已导出的部分(即 module.exports 的当前快照),继续执行;等 B 执行完,再回到 A 继续执行。
- A 开始执行 → 设置
exports.done = false→require('./b')中断 - B 开始执行 → 设置
exports.done = false→require('./a')返回此时 A 的module.exports(仅含done: false)→ B 继续执行并设done = true - A 恢复执行 → 此时读到 B 的
done: true→ 自己再设done = true
为什么会出现“未定义”相关报错
问题不在于导入语法本身,而在于你试图访问的导出项,在被 require 的那一刻还未被赋值。常见场景包括:
- 模块 B 依赖 A 中某个函数,但该函数是在 A 文件底部才定义并导出的,B 在中间就尝试调用 → 报
TypeError: xxx is not a function - 模块 A 导出一个对象,B 尝试解构其中某属性,但该属性尚未挂载到
exports上 → 报Cannot destructure property 'x' of 'undefined' - store 初始化时用了工具函数,工具函数又依赖 store 实例,但 store 还没 new 出来 → 访问
store.state报Cannot read property 'state' of undefined
如何定位和规避这类问题
关键不是“怎么让 CommonJS 不报错”,而是识别出哪些代码在循环链中过早使用了未就绪的导出。
- 检查报错堆栈,定位首次访问
undefined属性的位置,反推哪个模块在循环中提前消费了对方 - 避免在模块顶层直接调用对方导出的函数或访问深层属性;改用函数内调用,确保执行时机靠后
- 把共享逻辑抽离到第三个模块(如
shared/utils.js),打破 A↔B 直接依赖 - 对可能为空的对象访问加防御性判断,例如
if (a && typeof a.init === 'function') { a.init() } - 启用 Node.js 的
--trace-warnings启动参数,可捕获循环依赖警告(非错误,但提示风险)
和 ES6 模块的本质区别
ES6 模块用“活绑定”+静态分析,即使循环引用也能保证变量读取的是最新值;CommonJS 是“对象快照”+动态执行,一旦某字段没来得及写入,就永远是 undefined。所以同样的循环结构,ES6 可能运行成功,CommonJS 却因顺序问题失败。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











