标准答案
- CommonJS 是运行时加载,模块第一次执行时会放入缓存,循环依赖中另一方可能拿到一个尚未填完整的 exports 对象。
- ESM 的导入导出是静态结构,导入值是 live binding,会随着导出方更新而反映最新值。
- ESM 也不是完全没有循环依赖问题,如果在初始化完成前读取处于暂时性死区的绑定,仍可能报错。
- 循环依赖常见于工具函数互相调用、模块顶层执行逻辑过多、业务层和基础层边界混乱。
- 解决思路通常是抽出公共依赖、延迟执行、拆分职责,避免模块在顶层互相触发复杂逻辑。
题目解析
循环依赖最麻烦的地方不是“能不能 import”,而是模块初始化顺序。一个模块还没执行完,另一个模块就来读取它的导出,读到的可能是半成品,甚至直接触发初始化错误。
CommonJS 的表现通常更隐蔽:代码不一定立刻报错,但某个属性可能是 undefined。ESM 的静态绑定更明确,但如果顶层读取发生得太早,也会遇到初始化前访问的问题。
项目里不要靠记忆加载规则硬扛循环依赖。更可靠的处理方式是看依赖图,把共享能力下沉到第三个模块,或者把顶层执行改成函数调用时再发生。
代码示例
循环依赖里,顶层读取越多,越容易暴露初始化顺序问题。
JavaScript
// a.js
import { bValue } from './b.js'
export const aValue = 'a'
console.log(bValue)
// b.js
import { aValue } from './a.js'
export const bValue = 'b'
console.log(aValue)常见误区
- 认为 ESM 有 live binding,所以循环依赖一定安全。初始化时机仍然可能出问题。
- 只在报错位置加判空,没有处理模块职责互相缠绕的根因。
- 把大量业务逻辑放在模块顶层执行,让 import 本身产生复杂副作用。