【学习笔记】ECMAScript 词法环境全解析

词法环境规范

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 代码的词法嵌套结构,定义标识符与特定变量和函数之间的关联关系。

词法环境由两个组件构成:

  1. Environment Record:记录标识符绑定
  2. [[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 不是互相转化的关系,而是一个继承体系

plain 复制代码
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 个阶段

plain 复制代码
创建(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 查找时两边都查:

plain 复制代码
GlobalER.HasBinding(name):
  1. 先查 Declarative ER → 有则返回 true
  2. 再查 Object ER (即 window 对象) → 有则返回 true
  3. 都没有 → 返回 false
js 复制代码
// ------ 全局作用域: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,额外增加了字段:

需要注意两点:

  1. 箭头函数的 this 查找 :箭头函数没有自己的 [[ThisValue]],通过 [[OuterEnv]] 链向外查找包含 [[ThisValue]] 的 Function ER:

    js 复制代码
    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
        };
      },
    };
  2. 函数内的函数声明绑定到 Function ER :与全局函数声明进入 Object ER 不同,函数内部的函数声明通过 FunctionDeclarationInstantiation 绑定到当前 Function ER(继承自 Declarative ER),不挂载到全局对象:

    js 复制代码
    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 中绑定的间接引用(活绑定)

js 复制代码
// 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") → 返回当前值
}
js 复制代码
// 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 将标识符绑定映射到一个对象的属性,在两种场景中使用:

  1. 全局作用域 :作为 Global ER 的 [[ObjectRecord]],承载 var 和函数声明,[[BindingObject]]window
  2. 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]] 内部槽中。简单来说:函数在定义的那一刻,就把当时的作用域"拍了张快照"存起来了。这就是闭包能访问外部变量的根本原因。

闭包的规范定义------函数对象持有对创建时词法环境的引用

js 复制代码
// 伪代码:函数创建过程
FunctionCreate(kind, ParameterList, Body, Scope, ...) {
  let F = new FunctionObject();
  F.[[Environment]] = Scope;   // ← 闭包的本质:保存创建时的词法环境
  F.[[FormalParameters]] = ParameterList;
  F.[[ECMAScriptCode]] = Body;
  return F;
}

[[OuterEnv]] 外部环境引用

[[OuterEnv]] 是词法环境的外部引用,它构成了作用域链:

js 复制代码
// 嵌套函数的词法环境链
innerEnv.[[OuterEnv]] → outerEnv.[[OuterEnv]] → globalEnv.[[OuterEnv]] → null

GetIdentifierReference 抽象操作

当引擎需要解析一个标识符时,调用 ResolveBinding,其核心是 GetIdentifierReference

js 复制代码
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)   // 递归向外查找

具体触发场景:

js 复制代码
// 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

js 复制代码
// CreateMutableBinding(name, canDelete)
// 创建绑定但不初始化 → 状态为 uninitialized

// InitializeBinding(name, value)
// 将绑定的状态从 uninitialized 变为 initialized

// GetBindingValue(name, strict)
// 如果绑定状态是 uninitialized → 抛出 ReferenceError

这就是 TDZ(暂时性死区) 的本质:变量已存在于环境记录中(因此不会沿作用域链向外查找),但尚未初始化(访问时抛错)。

常见陷阱

1. TDZ 陷阱 --- 在声明前访问 let/const

ini 复制代码
// ❌ 错误:在声明前访问,触发 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. 闭包陷阱 --- 循环中共享同一个变量绑定

css 复制代码
// ❌ 错误: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 丢失陷阱 --- 普通函数与箭头函数混用

lua 复制代码
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 陷阱 --- 动态插入作用域链,导致查找不可预测

javascript 复制代码
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 是引用而非拷贝

javascript 复制代码
// 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
相关推荐
青青家的小灰灰2 小时前
React 架构进阶:自定义 Hooks 的高级设计模式与最佳实践
前端·react.js·前端框架
Angelial2 小时前
Vite 性能瓶颈排查标准流程
前端
不要秃头啊2 小时前
别再谈提效了:AI 时代的开发范式本质变了
前端·后端·程序员
青青家的小灰灰2 小时前
深入理解事件循环:异步编程的基石
前端·javascript·面试
用泥种荷花2 小时前
【LangChain.js学习】 向量数据库(内存/持久化)
前端
simon_luv_pho2 小时前
一行代码把网页变成 AI Agent?
前端
兆子龙2 小时前
模块联邦(Module Federation)详解:从概念到手把手 Demo
前端·架构
ZFSS2 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
没想好d2 小时前
通用管理后台组件库-8-表格组件
前端