【学习笔记】词法环境与环境记录:规范演进与V8实现

4 阅读7分钟

梳理 ES3 → ES5 → ES6+ 中词法环境与环境记录模型的演进,以及 V8 的实际实现

ES3 时代(1999):一个执行上下文,一个变量对象

ES3 只有 varfunction 声明,模型很简单——每个执行上下文有一个变量对象(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 的世界里完全够用。但它有三个根本性缺陷:

  1. 一个 VO 无法表达块级作用域: 同一个函数内只有一个 VO,没法让 {} 块有独立的变量空间
  2. 没有"已创建但未初始化"的状态: 变量要么存在要么不存在,无法描述 TDZ
  3. 单一扁平结构: 无法区分全局 var 挂 window、let 不挂 window 等多态行为

ES5+ 需要的 5 种环境记录及其多态行为

  • GlobalER:var → window 属性(ObjectRecord),let → 不挂 window(DeclarativeRecord)
  • FunctionER:管理 [[ThisValue]][[NewTarget]],VO 没有 this 管理能力
  • ModuleER:顶层 this = undefined,import 是间接活绑定—,VO 不存在间接绑定概念
  • ObjectERwith 语句将标识符绑定到对象属性,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)
│     ├── FunctionERIS-A 继承,额外包含 [[ThisValue]]
│     └── ModuleERIS-A 继承
├── ObjectER(对象式,标识符 ↔ 对象属性)
└── GlobalER(组合类型)   ← HAS-A 组合
      ├── [[ObjectRecord]]:      ObjectERvarwindow 属性
      └── [[DeclarativeRecord]]: DeclarativeERlet/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(当前级,可切换)
──────────────────────────────────      ──────────────────────────────────
varES1 遗产)               letES6 新增)
function 声明   (ES1 遗产)               constES6 新增)
函数参数        (ES1 遗产)               classES6 新增)
argumentsES1 遗产)               importES6 新增)

规则: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:
LEDeclarativeER → 没有 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 RecordContext 对象的 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 元数据彻底压平了这个区分。学习时以"函数级 / 块级词法环境"理解即可。