Skip to content

第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_: → 全局 Context
  • outer 的栈帧已销毁
  • Context_Outer 还在堆上

阶段 3:fn 函数对象的 [[Environment]] 指向 Context_Outer

text
fn.[[Environment]] → Context_Outer

fn 通过 [[Environment]] 指针,“绑架”了 Context_Outer
使得 xy 无法被垃圾回收。

作用域链的物理实现

当你在 inner 中访问 x

js
console.log(x);

V8 的查找过程:

  1. inner[[Environment]] 开始
  2. 找到 Context_Outer
  3. variables_[0] 中查找 x
  4. 返回 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
  • Contextfn.[[Environment]] 引用
  • 即使 badClosure 执行完,Context 仍在

3. 性能优化的关键

  • V8 会尝试“上下文折叠”(Context Folding):
    • 如果变量未被捕获,直接在栈帧中分配,不创建 Context
    • 减少堆分配,提升性能

本章核心结论

变量不在栈上,而在堆上的 Context 对象中。

  • let/const/var 声明的变量,最终都存储在 Contextvariables_ 数组中。
  • 闭包之所以能“记住”外层变量,是因为它持有 [[Environment]] 指针,指向外层的 Context
  • Context 的生命周期由 GC 管理,只有当所有引用断开后才会被回收。