词法环境规范
ECMAScript 定义词法环境为:
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code.
翻译为:词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构,定义标识符与特定变量和函数之间的关联关系。
词法环境由两个组件构成:
- Environment Record:记录标识符绑定
- [[OuterEnv]]:指向外部词法环境的引用(全局环境为
null)
Environment Record 类型
规范定义了五种环境记录类型:
| 类型 | 用途 | 说明 |
|---|---|---|
| Declarative Environment Record | let/const/class、函数内的函数声明 | 将标识符直接绑定到值 |
| Object Environment Record | with 语句、全局 var、全局函数声明 | 将标识符绑定到对象属性(全局函数声明通过 CreateGlobalFunctionBinding 挂载到 window) |
| Function Environment Record | 函数调用 | 继承自 Declarative,额外包含 [[ThisValue]]、[[FunctionObject]]、[[HomeObject]](super 引用)、[[NewTarget]](检测 new 调用) |
| Global Environment Record | 全局环境 | 包含一个 Object Record(对应 var、全局函数声明)和一个 Declarative Record(对应 let/const/class) |
| Module Environment Record | ES 模块 | 继承自 Declarative,支持 import 绑定 |
注意:函数声明的绑定位置取决于所在作用域。在全局作用域中,函数声明通过
CreateGlobalFunctionBinding(ES2024 §16.1.7)进入 Object ER,行为与var一致(挂载到window)。在函数作用域中,函数声明通过FunctionDeclarationInstantiation(ES2024 §10.2.11)进入 Function ER(继承自 Declarative ER),不挂载到全局对象。
Environment Record 的继承关系
五种 ER 不是互相转化的关系,而是一个继承体系:
Environment Record(抽象基类 — 定义公共接口)
│
├── Declarative Environment Record(声明式 — 直接绑定标识符到值)
│ │
│ ├── Function Environment Record(函数式 — 增加 this/new.target/super)
│ │
│ └── Module Environment Record(模块式 — 增加 import 间接绑定)
│
├── Object Environment Record(对象式 — 标识符绑定到对象属性)
│
└── Global Environment Record(全局式 — 组合了 Object ER + Declarative ER)
Environment Record 的生命周期
每个 Environment Record 经历 4 个阶段:
创建(Create) → 绑定注册(Binding) → 使用(Access) → 销毁(Destroy)
| 阶段 | 规范操作 | 说明 |
|---|---|---|
| 创建 | 进入新作用域时自动创建 | 函数调用、进入块、加载模块、脚本启动 |
| 绑定注册 | CreateMutableBinding / CreateImmutableBinding | 在环境中注册标识符(此时 let/const 处于 uninitialized 状态 → TDZ) |
| 初始化 | InitializeBinding(name, value) | var/函数声明在创建阶段就初始化;let/const 在执行到声明语句时才初始化 |
| 使用 | GetBindingValue / SetMutableBinding | 读取和修改变量值 |
| 销毁 | 无显式操作,由 GC 负责 | 当没有任何引用指向该环境时被回收(闭包会延长生命周期) |
五种 ER 的协作通信机制
五种 ER 通过继承(共享接口)、组合(Global 包含两种 ER)、链接([[OuterEnv]] 链)、间接引用(Module 的 import binding)这四种方式协作。
Global Environment Record — 组合模式
HasBinding 查找时两边都查:
GlobalER.HasBinding(name):
1. 先查 Declarative ER → 有则返回 true
2. 再查 Object ER (即 window 对象) → 有则返回 true
3. 都没有 → 返回 false
// —— 全局作用域:var 和函数声明 → Object ER ——
var a = 1;
function foo() {}
window.a; // 1 ← Object ER,映射到 window
window.foo; // ƒ ← Object ER,映射到 window
// —— 全局作用域:let/const/class → Declarative ER ——
let b = 2;
window.b; // undefined ← Declarative ER,不映射到 window
// —— 函数作用域:函数内的函数声明 → Function ER(继承自 Declarative ER)——
function outer() {
function inner() {}
window.inner; // undefined ← 不挂载到 window,绑定在 outer 的 Function ER 中
}
Function Environment Record — 继承 + 扩展
函数 ER 继承自 Declarative ER,额外增加了字段:
需要注意两点:
-
箭头函数的 this 查找:箭头函数没有自己的
[[ThisValue]],通过[[OuterEnv]]链向外查找包含[[ThisValue]]的 Function ER:const obj = { method() { // Function ER: { [[ThisValue]]: obj, [[ThisBindingStatus]]: initialized } const arrow = () => { // Declarative ER(箭头函数不创建 Function ER) // 访问 this → 沿 [[OuterEnv]] → 找到 method 的 Function ER → obj console.log(this); // obj }; }, }; -
函数内的函数声明绑定到 Function ER:与全局函数声明进入 Object ER 不同,函数内部的函数声明通过
FunctionDeclarationInstantiation绑定到当前 Function ER(继承自 Declarative ER),不挂载到全局对象:function outer() { // Function ER(继承自 Declarative ER) function inner() {} // → 绑定到 outer 的 Function ER var localVar = 1; // → 同样绑定到 outer 的 Function ER console.log(typeof inner); // "function" console.log(window.inner); // undefined ← 不挂载到 window } // 对比全局行为 function globalFn() {} // → Object ER → window.globalFn = ƒ console.log(window.globalFn); // ƒ globalFn()
Module Environment Record — 间接绑定
模块 ER 继承自 Declarative ER,新增 CreateImportBinding 方法,import 绑定是指向另一个模块 ER 中绑定的间接引用(活绑定):
// moduleA.js 的 Module ER
ModuleER_A {
count: 0, // 本地绑定
increment: <function> // 本地绑定
}
// moduleB.js 的 Module ER
ModuleER_B {
count: IndirectBinding → ModuleER_A.count // 间接绑定!
// GetBindingValue("count") 实际上是:
// → 跳转到 ModuleER_A → GetBindingValue("count") → 返回当前值
}
// moduleA.js
export let count = 0;
export function increment() {
count++;
}
// moduleB.js
import { count, increment } from './moduleA.js';
console.log(count); // 0 — 间接读取 ModuleER_A 的 count
increment(); // ModuleER_A 的 count 变为 1
console.log(count); // 1 — 再次间接读取,拿到最新值(活绑定)
Object Environment Record — with、全局 var 和全局函数声明
Object ER 将标识符绑定映射到一个对象的属性,在两种场景中使用:
- 全局作用域:作为 Global ER 的
[[ObjectRecord]],承载var和函数声明,[[BindingObject]]为window with语句:临时将对象包装为 Object ER 插入作用域链,[[BindingObject]]为with的参数对象
所有操作本质上都是对 [[BindingObject]] 的属性读写,这也是 var / function 声明会挂载到 window 的根本原因
完整协作流程
当执行一段代码时,各种 ER 如何协作:
[[Environment]] 内部槽
每个函数对象都有一个 [[Environment]] 内部槽
When a function is created, a reference to the Lexical Environment in which it was created is saved in its [[Environment]] internal slot.
翻译:当一个函数被创建时,它创建时所处的词法环境的引用会被保存在该函数的 [[Environment]] 内部槽中。简单来说:函数在定义的那一刻,就把当时的作用域"拍了张快照"存起来了。这就是闭包能访问外部变量的根本原因。
闭包的规范定义——函数对象持有对创建时词法环境的引用
// 伪代码:函数创建过程
FunctionCreate(kind, ParameterList, Body, Scope, ...) {
let F = new FunctionObject();
F.[[Environment]] = Scope; // ← 闭包的本质:保存创建时的词法环境
F.[[FormalParameters]] = ParameterList;
F.[[ECMAScriptCode]] = Body;
return F;
}
[[OuterEnv]] 外部环境引用
[[OuterEnv]] 是词法环境的外部引用,它构成了作用域链:
// 嵌套函数的词法环境链
innerEnv.[[OuterEnv]] → outerEnv.[[OuterEnv]] → globalEnv.[[OuterEnv]] → null
GetIdentifierReference 抽象操作
当引擎需要解析一个标识符时,调用 ResolveBinding,其核心是 GetIdentifierReference
GetIdentifierReference(env, name, strict):
1. If env is null, return a Reference Record { [[Base]]: unresolvable, ... }
2. Let exists = env.HasBinding(name)
3. If exists is true:
return Reference Record { [[Base]]: env, [[ReferencedName]]: name, ... }
4. Else:
let outer = env.[[OuterEnv]]
return GetIdentifierReference(outer, name, strict) // 递归向外查找
具体触发场景:
// 1. 读取变量 → 解析 x
console.log(x);
// 2. 赋值 → 解析 x(左侧也需要解析,得到 Reference Record 才能写入)
x = 5;
// 3. 函数调用 → 解析 foo
foo();
// 4. 运算表达式 → 解析 a 和 b
a + b;
// 5. typeof → 解析 y(特殊:unresolvable 不抛错,返回 "undefined")
typeof y;
简单来说:只要代码里出现了一个名字(不是属性访问的 . 后面那个),就触发一次 GetIdentifierReference。
obj.prop 中 obj 会触发,但 .prop 不会——属性访问走的是 [[Get]],不走环境链查找。
TDZ 的规范定义
let/const 声明的变量在环境记录中的初始状态为 uninitialized
// CreateMutableBinding(name, canDelete)
// 创建绑定但不初始化 → 状态为 uninitialized
// InitializeBinding(name, value)
// 将绑定的状态从 uninitialized 变为 initialized
// GetBindingValue(name, strict)
// 如果绑定状态是 uninitialized → 抛出 ReferenceError
这就是 TDZ(暂时性死区) 的本质:变量已存在于环境记录中(因此不会沿作用域链向外查找),但尚未初始化(访问时抛错)。
常见陷阱
1. TDZ 陷阱 — 在声明前访问 let/const
// ❌ 错误:在声明前访问,触发 TDZ
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;
// ✅ 正确:var 不存在 TDZ,只是 undefined
console.log(b); // undefined
var b = 1;
// ❌ 容易忽略的场景:函数参数默认值中的 TDZ
function foo(x = y, y = 2) { // ReferenceError: y 在 x 初始化时还处于 TDZ
return x + y;
}
foo();
2. 闭包陷阱 — 循环中共享同一个变量绑定
// ❌ 错误:var 只有一个绑定,所有回调共享同一个 i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出:3 3 3
// 原因:var i 在 Function ER 中只有一份绑定,
// 循环结束时 i 已经是 3,三个箭头函数读到的都是同一个 i
// ✅ 正确:let 每次迭代创建新的 Declarative ER,各自持有独立的 i 绑定
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出:0 1 2
3. this 丢失陷阱 — 普通函数与箭头函数混用
const obj = {
name: 'obj',
// ❌ 错误:箭头函数没有自己的 [[ThisValue]]
// 沿 [[OuterEnv]] 向外找到全局 Function ER,this 为 window/undefined(strict)
arrowMethod: () => {
console.log(this.name); // undefined(严格模式下报错)
},
// ✅ 正确:普通函数创建 Function ER,[[ThisValue]] 绑定为调用时的 obj
normalMethod() {
console.log(this.name); // 'obj'
// ✅ 内部箭头函数沿 [[OuterEnv]] 找到 normalMethod 的 Function ER → this 为 obj
const inner = () => console.log(this.name);
inner(); // 'obj'
},
};
obj.arrowMethod();
obj.normalMethod();
// ❌ 方法赋值后调用,Function ER 的 [[ThisValue]] 重新绑定为 undefined(严格模式)
const fn = obj.normalMethod;
fn(); // TypeError 或 window.name(非严格模式)
4. with 陷阱 — 动态插入作用域链,导致查找不可预测
const obj = { a: 1 };
const a = 2;
with (obj) {
// Object ER 被插入作用域链最顶层
// GetIdentifierReference 先查 obj 的属性,再查外部
console.log(a); // 1 ← 读取的是 obj.a,而非外部的 a = 2
}
// ❌ 动态属性导致歧义:无法在编译期确定标识符归属
const obj2 = {};
with (obj2) {
console.log(a); // 2 ← obj2 没有 a,向外找到外部的 a = 2
}
// 同一个标识符 a,with 不同对象结果不同,引擎无法优化,严格模式直接禁止 with
5. 模块活绑定陷阱 — import 是引用而非拷贝
// moduleA.js
export let count = 0;
export function increment() { count++; }
// moduleB.js
import { count, increment } from './moduleA.js';
console.log(count); // 0
increment();
console.log(count); // 1 ← 活绑定,读取的是 ModuleER_A 中 count 的当前值
// ❌ 常见误区:以为 import 的是值的拷贝,实则是间接引用
// ❌ import 绑定是只读的,不能直接赋值
count = 10; // TypeError: Assignment to constant variable