ES Modules通过静态分析、实时绑定和分阶段执行处理循环引用,访问未初始化的let/const会报ReferenceError,而函数/类可安全调用;其设计旨在暴露问题而非掩盖,倒逼清晰依赖。

ES Modules(ESM)不“解决”循环引用报错,而是通过**静态分析 + 实时绑定 + 分阶段执行**机制,让循环引用在多数情况下能安全运行——但若访问未初始化的 let/const 变量,仍会明确报 ReferenceError。这不是缺陷,而是设计使然:它把问题暴露出来,倒逼开发者写出更清晰的依赖逻辑。
ESM 的三阶段加载机制是关键
ESM 在执行前严格分为三个阶段:
-
解析(Parse):扫描所有
import语句,建立模块图,确定依赖关系,不执行任何代码; - 实例化(Instantiate):为每个模块分配内存空间,创建导出绑定(live bindings),此时变量已声明但尚未赋值(处于 TDZ);
- 求值(Evaluate):按拓扑顺序(从无依赖的模块开始)逐个执行模块顶层代码。
循环引用不会阻断解析或实例化,但求值顺序决定了哪些变量在何时可用。
为什么有时打印 undefined,有时直接报错?
取决于你访问的是什么类型的导出:
立即学习“Java免费学习笔记(深入)”;
- 如果导入的是
export let x或export const x,在被导入模块的求值完成前访问,会触发ReferenceError: Cannot access 'x' before initialization(因 TDZ); - 如果导入的是
export function f()或export class C,它们在实例化阶段就已“就绪”,可安全调用(函数声明提升,类也类似); - 如果导入的是
export { x as y }且x是var声明(不推荐),可能得undefined,但这不是标准写法。
真正有效的应对方式
不是绕过错误,而是重构为可预测的行为:
-
用函数封装导出值:把
export const value = ...改成export const getValue = () => ...,调用时再计算,避开 TDZ; - 避免顶层直接读取对方变量:把跨模块的数据获取逻辑移到函数体内,而非模块级语句中;
- 提取共享状态到独立模块:把循环双方都依赖的配置、工具或状态抽到第三个模块,单向依赖;
-
利用默认导出函数或类:相比命名导出的裸变量,
export default class或export default () => {...}更健壮,不受求值时机影响。
和 CommonJS 的本质区别
CommonJS 是运行时同步加载,靠缓存一个“半成品对象”硬扛循环,容易静默失败(比如拿到 {} 却不报错);ESM 是编译期静态分析,用明确的 ReferenceError 标记危险时机——它不掩盖问题,而是让问题可定位、可修复。


















