第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 的过程:
- 从
inner的[[Environment]]开始 - 跳转到
Context_outer - 在
variables_中找a❌(没找到) - 沿
previous_指针跳转到Context_global - 找到
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找的是outer的x,不是caller的
作用域链是“定义时决定”,调用栈是“运行时形成”。
2. 性能优化:内联缓存(Inline Caching)
V8 会优化变量查找:
- 第一次查找
x:遍历[[Environment]]链 - 缓存
x在Context中的 slot 索引(如variables_[0]) - 后续访问直接通过索引读取,O(1) 时间
所以“频繁访问的变量”会越来越快。
3. 内存泄漏预防
- 如果一个闭包长期持有大
Context,就会阻止 GC - 解决方案:主动断开引用
js
counter = null; // 断开 increment.[[Environment]] 的引用
// 此时 Context_createCounter 可被回收本章核心结论
作用域链的本质,是一条由 [[Environment]] 和 previous_ 指针连接的物理链表。
[[Environment]]是函数的“出生地证明”,指向定义时的词法环境。- 变量查找就是沿着这条指针链“逐级寻亲”。
- 闭包的“记忆能力”,源于
[[Environment]]对Context的长期引用。