闭包生命周期由堆中词法环境记录的引用关系决定,而非执行上下文栈;函数返回后,ECStack清空,但闭包通过[[Environment]]持续引用堆中环境记录,使其免于垃圾回收。

执行上下文栈(ECStack)本身不决定闭包的生命周期,它只管理函数何时开始执行、何时结束;真正支撑闭包长期存活的,是堆内存中被保留的词法环境记录,以及闭包函数内部的 [[Environment]] 引用。
执行上下文栈只负责“进与出”,不保留变量
当一个函数调用发生时,JS 引擎会创建对应的函数执行上下文,并将其压入 ECStack;函数返回后,该上下文立即被弹出、销毁。这个过程非常短暂——栈帧(stack frame)不会残留。
这意味着:如果变量真存放在栈上,outer 函数一返回,x 就该消失;但闭包还能读到 x,说明 x 并不在栈里。
- ECStack 中只存执行上下文的引用(如 { LexicalEnvironment, VariableEnvironment, ThisBinding })
- 而 LexicalEnvironment 本身是一个对象,分配在堆内存中
- 函数返回后,ECStack 清空,但堆中的环境记录只要还有引用(比如被闭包持有),就不会被垃圾回收
闭包靠 [[Environment]] 持有堆中环境记录的引用
每个函数对象在创建时,就通过内部槽 [[Environment]] 绑定了它定义时所处的词法环境。这个绑定是静态的、不可变的。
例如:
function outer() {let x = 10;
return function inner() { return x; };
}
const fn = outer(); // outer 执行完,其执行上下文已出栈
fn(); // 仍能访问 x → 因为 inner.[[Environment]] 指向 outer 创建的那个堆环境记录
- 多个闭包可共享同一堆环境记录(如计数器例子中的 inc 和 get)
- 修改 let/const 变量,是直接更新堆中该环境记录的值,不是拷贝
- 若变量是对象,所有闭包看到的是同一个对象实例(地址相同)
闭包生命周期 = 堆环境记录的存活周期
闭包是否“还活着”,取决于有没有活跃的函数对象通过 [[Environment]] 引用它。只要至少一个闭包存在,对应堆上的词法环境记录就不会被回收。
- 没有闭包引用 → 环境记录变成垃圾 → 下次 GC 时释放
- 有闭包引用(哪怕只是全局变量持有一个函数)→ 环境记录持续驻留堆中
- var / let / const 声明的变量,在闭包场景下都表现为堆中可被长期引用的绑定,区别仅在于初始化时机和作用域规则
常见误解澄清
✘ “闭包让变量留在栈上” —— 栈帧早已销毁,不存在“栈上残留”
✘ “闭包复制了变量值” —— 它捕获的是对环境记录的引用,不是值的副本
✘ “执行上下文栈控制闭包存活” —— ECStack 与闭包生命周期无直接因果关系,它只反映函数调用流程
闭包的本质,是函数与其定义时词法环境的组合;这个组合的生命力,由堆内存管理和引用计数机制保障,而非调用栈。

















