Skip to content

第3章:指针与作用域链 —— [[Environment]] 的物理实现

“作用域链不是逻辑链,而是由 [[Environment]] 指针连接的物理链表。”
—— 当你看到 ,你就看穿了变量查找的“寻路系统”。

[[Environment]] 是什么?—— 闭包的“锚点”

在 ECMAScript 规范中,每个函数对象都有一个内部槽(Internal Slot)

[[Environment]]: Context*

  • 类型:一个指向 Context 对象的指针
  • 赋值时机函数创建时(不是调用时!)
  • :函数定义位置的词法环境(即当时的 Context

关键点

  • [[Environment]]函数诞生那一刻就固定了
  • 它决定了闭包能访问哪些外层变量
  • 这就是“词法作用域”的物理基础

作用域链的物理结构:一条指针链

我们来看一个典型的嵌套函数:

js
function globalFn() {
  let a = 1;

  function outer() {
    let b = 2;

    function inner() {
      let c = 3;
      console.log(a, b, c); // 查找 a 和 b
    }

    inner();
  }

  outer();
}

globalFn();

当执行到 console.log(a, b, c) 时,内存中的作用域链是这样的:

text
[inner 函数对象]
  [[Environment]] → Context_outer

                    ├─ variables_[0]: b = 2
                    └─ previous_ → Context_global

                                      ├─ variables_[0]: a = 1
                                      └─ previous_: null (或全局对象)

[栈帧:inner]
  context → Context_inner

            └─ variables_[0]: c = 3

查找 a 的过程:

  1. inner[[Environment]] 开始
  2. 跳转到 Context_outer
  3. variables_ 中找 a ❌(没找到)
  4. 沿 previous_ 指针跳转到 Context_global
  5. 找到 a = 1

这就是“沿作用域链向上查找”的物理实现

再看闭包:[[Environment]] 如何“锁定”外层环境

js
function createCounter() {
  let count = 0;
  return function increment() {
    count++;
    return count;
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2

关键时刻:createCounter 返回前

[堆内存]
Context_createCounter:
  variables_[0]: count = 0
  previous_: → 全局 Context

[increment 函数对象]
  [[Environment]] → Context_createCounter  ← 指针已建立!
  • increment 函数被创建时,[[Environment]] 指向当前的 Context_createCounter
  • 即使 createCounter 执行完,栈帧销毁,Context_createCounter 依然存在
  • 因为 increment.[[Environment]] 还指着它

所以 count 不会丢失,每次调用 counter() 都在修改同一个 count

为什么这很重要?

1. 作用域链 ≠ 调用栈

作用域链调用栈
基于词法环境(定义时)执行顺序(运行时)
物理实现[[Environment]] + previous_ 指针链栈帧的 push/pop
变化频率函数创建时确定,永不改变每次函数调用都变

示例:

js
function outer() {
  let x = "outer";
  return function inner() {
    console.log(x);
  };
}

function caller(fn) {
  let x = "caller";
  fn(); // 输出什么?
}

const inner = outer();
caller(inner); // 输出 "outer",不是 "caller"
  • 调用栈:caller → inner
  • 但作用域链:inner.[[Environment]] → outer 的 Context
  • 所以 x 找的是 outerx,不是 caller

作用域链是“定义时决定”,调用栈是“运行时形成”。

2. 性能优化:内联缓存(Inline Caching)

V8 会优化变量查找:

  • 第一次查找 x:遍历 [[Environment]]
  • 缓存 xContext 中的 slot 索引(如 variables_[0]
  • 后续访问直接通过索引读取,O(1) 时间

所以“频繁访问的变量”会越来越快。

3. 内存泄漏预防

  • 如果一个闭包长期持有大 Context,就会阻止 GC
  • 解决方案:主动断开引用
js
counter = null; // 断开 increment.[[Environment]] 的引用
// 此时 Context_createCounter 可被回收

本章核心结论

作用域链的本质,是一条由 [[Environment]]previous_ 指针连接的物理链表。

  • [[Environment]] 是函数的“出生地证明”,指向定义时的词法环境。
  • 变量查找就是沿着这条指针链“逐级寻亲”。
  • 闭包的“记忆能力”,源于 [[Environment]]Context 的长期引用。