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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 06:45:31