Node.js跨模块循环依赖为何会导致运行时函数为undefined报错
问题根本原因
该错误是Node.js CommonJS模块规范的循环依赖加载机制导致的,和Java等编译型语言的符号解析逻辑有本质区别:
CommonJS模块是同步运行时加载,为了避免循环引入时无限递归,Node加载模块时会先在缓存中写入一个空的exports对象副本,再执行模块代码。本次的加载执行顺序如下:
index.js引入moduleA,Node先将moduleA的空exports对象写入缓存,开始执行moduleA代码moduleA第一行就引入moduleB,暂停自身执行,转向加载moduleB,同样先将moduleB的空exports写入缓存moduleB第一行引入moduleA,发现缓存中已有moduleA的记录,直接返回当前的空exports对象。此时moduleB顶部执行const {a1} = require('./moduleA')解构得到的a1为undefined,且该值会被静态保留moduleB执行完全部代码,导出完整的{b1, b2}对象,回到moduleA的执行流程moduleA继续执行,定义a1、a2后才给exports赋值为{a1, a2},但此时moduleB中已经保存的a1值仍为之前的undefined- 调用
a2()触发b1()执行时,调用a1()就会抛出TypeError: a1 is not a function的错误。

无需拆分模块的解决方法
以下三种方案都可以在不拆分现有模块的前提下解决问题:
方案1:函数内部延迟引入
不要在模块顶部解构引入依赖,改为在函数实际执行时再引入,此时被依赖模块已经完成全量加载,能拿到完整的导出值:
修改moduleB.js代码即可:
// moduleB.js const b1 = () => { // 运行时再引入moduleA,此时moduleA已完成导出 const {a1} = require('./moduleA'); console.log('b1 called'); a1(); }; const b2 = () => { console.log('b2 called'); }; module.exports = {b1, b2};
方案2:引用完整导出对象,避免顶层解构
CommonJS导出的exports是引用传递,只要不解构拷贝值,直接持有exports对象的引用,后续模块更新导出内容时也能拿到最新值:
修改moduleB.js代码:
// moduleB.js 引入完整的moduleA导出对象,不解构 const moduleA = require('./moduleA'); const b1 = () => { console.log('b1 called'); // 调用时从moduleA引用上取最新的a1 moduleA.a1(); }; const b2 = () => { console.log('b2 called'); }; module.exports = {b1, b2};
方案3:切换为ES Module(ESM)规范
ESM是ECMAScript官方模块规范,采用编译期静态符号绑定,循环依赖时绑定的是导出值的引用而非拷贝,天然支持这类场景。
只需在项目package.json中添加"type": "module"配置,将所有require改为import语法,module.exports改为export语法即可:
// moduleA.js import {b1} from './moduleB.js'; export const a1 = ()=>{ console.log('a1 called') }; export const a2 = ()=>{ console.log('a2 called'); b1() };
// moduleB.js import {a1} from './moduleA.js'; export const b1 = () => { console.log('b1 called'); a1(); }; export const b2 = () => { console.log('b2 called'); };
修改后运行即可得到预期输出。
内容的提问来源于stack exchange,提问作者Sean
相关产品推荐
相关产品推荐

