es6模块的live binding本质是内存地址共享,即所有导入方共享同一绑定槽的实时引用;它在实例化阶段建立,支持export let/var/function/class及export default表达式,但不适用于export const或字面量导出。

ES6 模块的 live binding(活绑定)不是“变量能被改”,而是“所有导入方共享同一内存位置的实时引用”。它不依赖开发者手动同步,而是由模块系统在实例化阶段就建立的底层绑定机制。
活绑定的本质是内存地址共享
当一个模块用 export let / var / function / class 声明导出项时,引擎在“实例化”阶段就为该变量创建一个可读写的绑定槽(binding slot),所有 import 语句拿到的都不是值副本,而是指向这个槽的引用。修改任一端,其他端立即可见。
- export let count = 0; → 所有 import { count } 的模块看到的是同一个 count 绑定
- export const PI = 3.14; → const 声明不可重新赋值,但绑定依然存在;导入方无法 count = 5,但可 count++(如果 export 是 let)
- export default { name: 'a' }; → 导入的是对象引用,可修改属性(obj.name = 'b'),但不能 obj = {}(会报错,因 default 绑定不可重赋)
活绑定在循环依赖中依然生效
模块 A 导出 let x = 1,同时 import B;B 又 import A 并读取 x。即便 A 的顶层代码尚未执行完,B 中的 x 已是一个已声明但未初始化的绑定(初始值为 undefined)。一旦 A 后续执行 x = 2,B 立刻读到 2 —— 这不是事件监听,而是同一内存地址的自然反映。
- 实例化阶段先预留所有 export 变量的绑定位置,求值阶段才填入实际值
- 因此即使模块尚未完全执行,导入方也能安全持有引用,不会报 ReferenceError
- CommonJS 的值拷贝做不到这点:require('a') 在 B 中拿到的是快照,后续 A 改变 x 对 B 无影响
哪些导出支持活绑定?
活绑定只对顶层声明的可变绑定生效,不是所有 export 都“活”:
- ✅ export let / var / function / class → 绑定可变,导入方可读写(符合声明限制)
- ✅ export default 表达式(如 export default (() => {})())→ 表达式仅执行一次,结果被活绑定
- ❌ export const / export 字面量(如 export const VERSION = '1.0')→ 绑定存在,但不可重赋;导入方看到的是稳定值,非“活”在行为上
- ❌ export default 对象字面量本身(export default { a: 1 })→ 对象可修改属性,但整个 default 绑定不可替换
活绑定与模块缓存密不可分
live binding 能成立的前提,是模块只执行一次、导出对象被复用。如果每次 import 都新建模块实例,绑定就失去意义。
- 同一规范路径(如 './utils.js')无论 import 多少次,只有一份导出对象
- import { a } from './x.js' 和 import { b } from './x.js' 共享同一模块实例,a 和 b 的修改彼此可见
- 路径差异(如 './x.js' vs '../x.js')哪怕物理文件相同,也触发独立执行和独立绑定











