CommonJS与ES Module执行顺序及循环依赖差异解析
CommonJS与ES Module循环导入对比测试
前置准备
通过npm init -y创建Node.js项目,默认使用CommonJS规范;若切换为ES Module,需修改package.json添加"type": "module"。
CommonJS测试1
index.js
const {person2,greetingFromPerson2} = require('./app.js'); const person1 = 'Abraham'; module.exports = person1; console.log(`${person1} greets ${person2}`); greetingFromPerson2();
app.js
const person1 = require('./index.js'); const person2 = 'John'; const greetingFromPerson2 = () =>{ console.log(`${person2} greets ${person1}`); } module.exports = {person2,greetingFromPerson2};
执行结果(node index.js)
Abraham greets John Abraham greets John John greets [object Object] (node:3856) Warning: Accessing non-existent property 'Symbol(Symbol.toPrimitive)' of module exports inside circular dependency (Use `node --trace-warnings ...` to show where the warning was created) (node:3856) Warning: Accessing non-existent property 'Symbol(Symbol.toStringTag)' of module exports inside circular dependency
CommonJS测试2
修改后的index.js
let person1; module.exports = person1; const {person2,greetingFromPerson2} = require('./app.js'); person1 = 'Abraham'; console.log(`${person1} greets ${person2}`); greetingFromPerson2();
执行结果(node index.js)
Abraham greets John John greets undefined
假设验证:app.js导入的person1是index.js被require时的变量状态快照,该假设成立。
ES Module测试
配置修改
修改package.json添加"type": "module",切换为ES Module规范。
index.js
import { person2, greetingFromPerson2 } from './app.js'; let person1 = 'Abraham'; console.log(`${person1} greets ${person2}`); export default person1; greetingFromPerson2();
app.js
import person1 from './index.js'; export const person2 = 'John'; export const greetingFromPerson2 = () =>{ console.log(`${person2} greets ${person1}`); }
执行结果(node index.js)
Abraham greets John John greets Abraham
问题解答
1. CommonJS与ES Module的执行顺序差异
- CommonJS:模块加载为同步执行,加载时先创建模块的
module对象,随后同步执行模块内代码,遇到require会立即加载并执行目标模块,执行完成后返回目标模块的module.exports。循环导入时,返回的是当前已执行部分的module.exports快照,后续模块内变量修改不会同步到已导入的引用。 - ES Module:模块加载为异步解析+同步执行,先完成模块依赖的解析(构建阶段),确定所有导出关系后,再执行模块代码(执行阶段)。循环导入时,导入的是模块导出的引用绑定而非快照,后续变量修改会实时同步到导入的引用上。
2. 为何CommonJS下app.js无法获取index.js的person1
这是CommonJS循环导入的特性导致:
- 测试1中,index.js先执行
require('./app.js'),此时index.js还未执行到const person1 = 'Abraham'和module.exports = person1,app.js导入的是index.js当前的module.exports(初始为空对象),后续index.js给module.exports赋值person1时,app.js里的person1引用已是之前的空对象快照。 - 测试2中,index.js先把
undefined的person1赋值给module.exports,再执行require('./app.js'),app.js导入的是这个undefined的快照;后续index.js修改person1变量,不会改变app.js里已导入的快照值——因为CommonJS导出的是值的拷贝,而非引用。
3. 第一个输出中警告的含义
该警告是Node.js处理CommonJS循环依赖时的提示:
循环导入发生时,app.js导入的index.js模块未完成初始化,其module.exports是未完全初始化的对象。代码尝试将这个对象进行字符串拼接(${person2} greets ${person1}),JavaScript会调用对象的Symbol.toPrimitive和Symbol.toStringTag属性做类型转换,但此时这些属性还未被添加到模块导出对象上,因此触发警告,提示在循环依赖中访问了不存在的属性。
4. 类比说明CommonJS与ES Module导出机制的差异
- CommonJS的
module.exports:相当于快递包裹,你把要导出的内容打包好(赋值给module.exports),别人用require拿到的是这个包裹的拷贝。后续你修改自己手里的包裹内容,别人的拷贝不会变化;如果一开始包裹是空的,别人拿到的就是空包裹,之后你再往自己的包裹塞东西,别人的还是空的。无论导出单个值还是对象,本质都是值的拷贝。 - ES Module的
export/export default:相当于共享储物柜,你把东西放在储物柜里,别人用import拿到的是储物柜的钥匙。不管你什么时候修改储物柜里的内容,别人用钥匙打开看到的都是最新状态。导出的是变量的引用绑定而非值的拷贝,变量更新会实时同步给所有导入者。
内容的提问来源于stack exchange,提问作者John George
相关产品推荐
相关产品推荐

