梳理 ES3 → ES5 → ES6+ 中词法环境与环境记录模型的演进,以及 V8 的实际实现
ES3 时代(1999):一个执行上下文,一个变量对象
ES3 只有 var 和 function 声明,模型很简单——每个执行上下文有一个变量对象(Variable Object, VO),函数调用时叫活动对象(Activation Object, AO):
ExecutionContext (ES3) {
VariableObject: { ... } // 一个扁平对象,存所有变量
ScopeChain: [VO, 外层VO, ..., GlobalVO] // 数组式作用域链
ThisBinding: <this>
}
function foo(a) {
var x = 1;
function bar() {}
}
// ES3 的 AO(活动对象):所有东西放在一个对象里,简单粗暴
AO = {
a: 1,
x: undefined, // var 提升
bar: <function>, // 函数声明提升
arguments: { 0: 1, length: 1 }
}
查找变量:ScopeChain 构建时将当前 AO 前插到数组头部([AO].concat([[scope]])),查找时从索引 0(最内层)向后遍历到末尾(全局),即从内到外,找到就返回。
这个模型够用吗? 在只有 var 的世界里完全够用。但它有三个根本性缺陷:
- 一个 VO 无法表达块级作用域: 同一个函数内只有一个 VO,没法让
{}块有独立的变量空间 - 没有"已创建但未初始化"的状态: 变量要么存在要么不存在,无法描述 TDZ
- 单一扁平结构: 无法区分全局 var 挂 window、let 不挂 window 等多态行为
ES5+ 需要的 5 种环境记录及其多态行为:
- GlobalER:var →
window属性(ObjectRecord),let → 不挂window(DeclarativeRecord)- FunctionER:管理
[[ThisValue]]、[[NewTarget]],VO 没有 this 管理能力- ModuleER:顶层
this = undefined,import 是间接活绑定—,VO 不存在间接绑定概念- ObjectER:
with语句将标识符绑定到对象属性,VO 没有"绑定到另一个对象"的抽象- DeclarativeER:支持
uninitialized状态(TDZ),VO 中变量要么存在要么不存在
ES5 时代(2009):引入双指针,为块级作用域做准备
ES5 预见到 let/const 的到来,将 VO/AO 替换为更精细的架构:
术语替换:
ES3 ES5+
───────────────────── ──────────────────────────
Variable Object (VO) → VariableEnvironment(指针)
Activation Object (AO) → Environment Record(环境记录)
Scope Chain(数组) → [[OuterEnv]] 链(链表)
为什么作用域链要从数组换成链表?
ES3 数组式:
ScopeChain = [当前VO, 外层VO, ..., GlobalVO]
函数创建时一次性构建好整个数组,之后不变
进入 {} 块?→ 无法在数组中间插入新节点
ES5+ 链表式:
当前环境.[[OuterEnv]] → 外层环境.[[OuterEnv]] → ... → GlobalEnv → null
进入 {} 块 → 创建新节点,[[OuterEnv]] 指向当前环境 ← 插入
退出 {} 块 → 丢弃当前节点,恢复到 [[OuterEnv]] ← 摘除
数组做不到动态插入/移除节点(本质是数组没有"当前位置"的概念),链表可以。这就是 ES6 块级作用域的前提——每个 {} 在链上插一个新节点,退出就摘掉。ES5 提前把数据结构换好,ES6 才能在这个基础上实现 let/const。
新架构:
ExecutionContext (ES5) {
VariableEnvironment: LexicalEnvironment // 指针 1:固定,指向函数级
LexicalEnvironment: LexicalEnvironment // 指针 2:可变,随块切换
ThisBinding: <this>
}
LexicalEnvironment {
record: EnvironmentRecord // 具体的存储容器
[[OuterEnv]]: LexicalEnvironment | null // 取代了 ScopeChain 数组
}
同时引入了 Environment Record 的类型层次:
EnvironmentRecord(抽象基类)
├── DeclarativeER(声明式,存储 let/const/class)
│ ├── FunctionER ← IS-A 继承,额外包含 [[ThisValue]]
│ └── ModuleER ← IS-A 继承
├── ObjectER(对象式,标识符 ↔ 对象属性)
└── GlobalER(组合类型) ← HAS-A 组合
├── [[ObjectRecord]]: ObjectER → var → window 属性
└── [[DeclarativeRecord]]: DeclarativeER → let/const → 不挂 window
关键:FunctionER 继承自 DeclarativeER,GlobalER 组合 ObjectER 和 DeclarativeER
但 ES5 时代还没有 let/const,所以两个指针始终指向同一个环境,这个架构基本用不上:
ES5 时代实际运行:
VariableEnvironment ──→ ┌──────────────┐
│ FunctionER │ ← 始终相同,分不分无所谓
LexicalEnvironment ──→ └──────────────┘
// 唯一能让两者分离的是 with 语句(已不推荐使用)
ES6 时代(2015):let/const 到来,双指针正式生效
ES6 引入 let/const/class,双指针架构终于派上用场:
分工明确:
VariableEnvironment(函数级,固定) LexicalEnvironment(当前级,可切换)
────────────────────────────────── ──────────────────────────────────
var (ES1 遗产) let (ES6 新增)
function 声明 (ES1 遗产) const (ES6 新增)
函数参数 (ES1 遗产) class (ES6 新增)
arguments (ES1 遗产) import (ES6 新增)
规则:ES6 之前的语法走 VE,ES6 新增的语法走 LE。
运行机制
function foo() {
var x = 1;
let y = 2;
{
let z = 3;
var w = 4; // var 穿透块!
}
}
第一步:函数开始,两个指针指向同一个环境
VE ──→ FunctionER { x: undefined, w: undefined, y: <TDZ> }
LE ──→ 同上
第二步:执行赋值
VE ──→ FunctionER { x: 1, w: undefined, y: 2 }
LE ──→ 同上
第三步:进入 {} 块,LE 切换,VE 不变
VE ──→ FunctionER { x: 1, w: undefined, y: 2 } ← 不变
LE ──→ DeclarativeER { z: <TDZ> } ← 切换了!
[[OuterEnv]] → FunctionER
第四步:块内执行
VE ──→ FunctionER { x: 1, w: 4, y: 2 } ← var w 穿透到这里
LE ──→ DeclarativeER { z: 3 }
[[OuterEnv]] → FunctionER
第五步:退出块,LE 恢复
VE ──→ FunctionER { x: 1, w: 4, y: 2 }
LE ──→ FunctionER(恢复) ← 恢复
// DeclarativeER { z: 3 } 可被 GC 回收
创建绑定用哪个指针
var w = 4→ 通过 VE 找到 FunctionER → 绑定在函数级(穿透块)let z = 3→ 通过 LE 找到 DeclarativeER → 绑定在块级
查找变量只走 LE 的链
在块内查找 x:
LE → DeclarativeER → 没有 x → [[OuterEnv]] → FunctionER → 找到 x
两个指针指向什么类型的 ER
VariableEnvironment LexicalEnvironment
─────────────────── ────────────────────
全局 GlobalER GlobalER / DeclarativeER(块内)
函数顶层 FunctionER FunctionER(和 VE 相同)
函数内 {} 块 FunctionER(不变) DeclarativeER(切换了)
简单来说:只有有块才创建,没块者相同
ES6+ 现代理解:用一条链替代双指针
双指针模型 一条链模型
───────────── ─────────────
VE 指向函数级环境 → var 绑定在函数级词法环境
LE 指向当前级环境 → let/const 绑定在块级词法环境
LE 随 {} 块切换 → 进入块创建新环境节点
VE 不变 → var 无论在哪声明,都绑定到函数级那层
[[OuterEnv]] 链 → 同
每个 {} = 新建一个 DeclarativeER 节点,[[OuterEnv]] = 节点之间的连线,查找 = 从当前节点沿连线往上找:
function foo(a) {
var x = 1;
let y = 2;
{
let z = 3;
{
let w = 4;
}
}
}
┌──────────────────────────┐
│ FunctionER │ ← 函数级(var/参数/function 在这)
│ a, x, y, arguments │
│ [[OuterEnv]] → GlobalER │
└───────────▲──────────────┘
│ [[OuterEnv]]
┌───────────┴──────────────┐
│ DeclarativeER │ ← 第一个 {}(let/const 在这)
│ z │
└───────────▲──────────────┘
│ [[OuterEnv]]
┌───────────┴──────────────┐
│ DeclarativeER │ ← 第二个 {}
│ w │
└──────────────────────────┘
不需要两个指针,只需要:
- var → 绑定在函数级词法环境
- let/const → 绑定在当前块级词法环境
- 查找 → 从当前节点沿
[[OuterEnv]]往上找
V8 实现:从来没有两个环境对象
V8 从 ES5 到 ES6+,始终没有实现过 VE/LE 双指针。它用自己的方式达到相同语义:
规范概念 V8 实现
──────────────────────────────────────────────────
VE / LE 双指针 → 不存在
Environment Record → Context 对象的 slots(变量存储槽)
[[OuterEnv]] 链 → Context.previous 指针
var vs let 区分 → ScopeInfo 的 variable mode(VAR / LET / CONST)
函数级 vs 块级 → ScopeInfo 的 scope_type(FUNCTION_SCOPE / BLOCK_SCOPE)
V8 编译阶段就决定好一切:
function foo() {
var x = 1; // ScopeInfo: { x: VAR, scope: FUNCTION, location: STACK }
let a = 2; // ScopeInfo: { a: LET, scope: FUNCTION, location: STACK }
{
let y = 3; // ScopeInfo: { y: LET, scope: BLOCK, location: CONTEXT }
return () => y; // y 被闭包引用 → 堆分配
}
}
栈帧 (foo) 堆
┌──────────────────┐
│ x = 1 (VAR) │ ┌───────────────────┐
│ a = 2 (LET) │ │ BlockContext │
│ Context Ptr ──────────────> │ previous → null │
└──────────────────┘ │ y = 3 (LET) │
└───────────────────┘
规则:
没被闭包引用 → 栈上(不分 var/let)
被闭包引用 → Context 对象(堆上)
var 的"穿透块"行为:编译时 V8 看到 var 的 scope_type 是 FUNCTION_SCOPE,直接分配到函数级栈帧,跳过所有 BlockContext。不需要 VE 指针来告诉它。
全景时间线
1999 ES3 VO/AO + ScopeChain 数组
│ 一个执行上下文一个变量对象,简单但无法扩展
│
2009 ES5 VariableEnvironment / LexicalEnvironment 双指针
│ + Environment Record 类型层次
│ + [[OuterEnv]] 链表替代 ScopeChain 数组
│ 架构就绪,但 let/const 还没来,双指针基本用不上
│
2015 ES6 let/const/class 到来,双指针正式生效
│ VE 存旧语法(var/function),LE 存新语法(let/const/class)
│ LE 随 {} 块切换,VE 固定不变
│
现代 ES6+ 社区简化为"词法环境嵌套"
│ 函数级词法环境(= VE 指向的)
│ 块级词法环境(= LE 切换到的)
│ 一条 [[OuterEnv]] 链,不需要双指针概念
│
引擎 V8 从来没实现双指针
ScopeInfo(编译时元数据)+ Context 链(运行时存储)
用 variable mode + scope_type 区分一切
总结:三层视角说的是同一件事
| 问题 | 规范层(ES5 双指针) | 现代理解(ES6+ 一条链) | V8 实现 |
|---|---|---|---|
| var 绑定到哪 | VE 指向的环境 | 函数级词法环境 | ScopeInfo: VAR + FUNCTION_SCOPE |
| let 绑定到哪 | LE 当前指向的环境 | 块级词法环境 | ScopeInfo: LET + BLOCK_SCOPE |
| 查找变量 | 从 LE 沿 [[OuterEnv]] 链 | 从当前层沿链向外 | 编译时确定偏移,直接读取 |
| 两个环境? | 两个指针,可指向同一个 | 一条链的不同层级 | 一条 Context 链 + ScopeInfo 元数据 |
核心结论:VariableEnvironment 本质上就是"函数级词法环境的固定书签"。ES6+ 用词法环境嵌套([[OuterEnv]] 链)替代了双指针模型,V8 用 ScopeInfo 元数据彻底压平了这个区分。学习时以"函数级 / 块级词法环境"理解即可。