第2章:Context 对象 —— 变量的真正归宿
“你以为 let x = 1 存在栈上?不,它在堆上,被 Context 精心保管。”
—— 当你看到 variables_ 数组,你就看穿了变量的“户籍系统”。
为什么需要 Context?—— 栈的局限性
我们上一章讲到,栈帧在栈内存中,函数返回即销毁。
但有一个问题:
如果变量都存在栈帧里,
那闭包怎么还能访问外层函数的变量?
js
function outer() {
let secret = "I'm hidden";
return function() {
console.log(secret); // 依然能访问!
};
}outer()执行完,它的栈帧被弹出(销毁)- 但
secret还能被访问 - 矛盾出现了!
所以,变量不能只存在栈上。
解决方案:Context 对象
V8 引擎的解决方案是:
把变量存在堆上,用 Context 对象统一管理。
- 位置:堆内存(Heap Memory)
- 特点:不会随函数返回而销毁,可被 GC 管理
- 作用:作为“变量容器”,被栈帧和闭包共同引用
Context 对象结构(V8 简化版)
cpp
class Context {
public:
// 1. 变量槽:存储实际变量
Object* variables_[kVariableCount];
// 例如:variables_[0] = "I'm hidden"
// 2. 外层链接:指向父级 Context
Context* previous_;
// 形成作用域链
// 3. 扩展区:用于优化(如内联缓存)
Object* extension_;
// V8 动态添加优化信息
};示例:闭包如何“绑架”变量
js
function outer() {
let x = 10;
let y = 20;
return function inner() {
console.log(x, y); // 捕获 x 和 y
};
}
const fn = outer(); // outer 执行结束
fn(); // 10 20 —— 变量依然存在内存结构演化:
阶段 1:outer() 执行中
[调用栈]
┌─────────────────────────────┐
│ [outer 的栈帧] │
│ receiver: window │
│ context: → Context_Outer │
└─────────────────────────────┘
[堆内存]
Context_Outer:
variables_[0]: x = 10
variables_[1]: y = 20
previous_: → 全局 Context阶段 2:outer() 返回,fn 被赋值
[调用栈]
┌─────────────────────────────┐
│ [全局栈帧] │
└─────────────────────────────┘
[堆内存]
Context_Outer: ← 依然存在!
variables_[0]: x = 10
variables_[1]: y = 20
previous_: → 全局 Contextouter的栈帧已销毁- 但
Context_Outer还在堆上
阶段 3:fn 函数对象的 [[Environment]] 指向 Context_Outer
text
fn.[[Environment]] → Context_Outerfn 通过 [[Environment]] 指针,“绑架”了 Context_Outer,
使得 x 和 y 无法被垃圾回收。
作用域链的物理实现
当你在 inner 中访问 x:
js
console.log(x);V8 的查找过程:
- 从
inner的[[Environment]]开始 - 找到
Context_Outer - 在
variables_[0]中查找x - 返回
10
这就是“作用域链”的物理形态:
一个由 previous_ 指针连接的 Context 链表。
为什么这很重要?
1. 闭包的本质
- 闭包 = 函数 +
[[Environment]]指针 - 指针指向堆上的
Context - 只要函数存在,
Context就不会被 GC 回收
2. 内存泄漏的根源
js
let data = new Array(1e6).fill('leak');
function badClosure() {
return function() {
console.log(data.length); // 捕获大对象
};
}
const fn = badClosure();
// 忘记 fn = null → data 永远不会被回收data存在Context中Context被fn.[[Environment]]引用- 即使
badClosure执行完,Context仍在
3. 性能优化的关键
- V8 会尝试“上下文折叠”(Context Folding):
- 如果变量未被捕获,直接在栈帧中分配,不创建
Context - 减少堆分配,提升性能
- 如果变量未被捕获,直接在栈帧中分配,不创建
本章核心结论
变量不在栈上,而在堆上的 Context 对象中。
let/const/var声明的变量,最终都存储在Context的variables_数组中。- 闭包之所以能“记住”外层变量,是因为它持有
[[Environment]]指针,指向外层的Context。 Context的生命周期由 GC 管理,只有当所有引用断开后才会被回收。