前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
环境记录是一种规范类型,环境记录通常与 ECMAScript 代码的某些特定语法结构相关联,例如 函数申明(FunctionDeclaration)、 块语句(BlockStatement) 等等。每次计算这些代码时,都会创建一个新的 Environment Record 来记录代码创建的标识符绑定。
每个环境记录都有一个[[ OuterEnv ]]字段,该字段要么为 null,要么是对外部环境记录的引用。
环境记录 是纯粹的规范机制,不需要与 ECMAScript 实现的任何特定构件相对应。
作用
- 标识符绑定: 声明环境记录负责维护标识符与其绑定的值之间的映射关系。当一个标识符在一个作用域内被声明时,相应的绑定会在声明环境记录中创建。
- 作用域链: 当创建一个新的声明环境记录时,它通常会关联一个外部环境记录,形成一个作用域链。这使得内部作用域能够访问外部作用域中的标识符绑定。
- 闭包: 从协议上来看,闭包本质是函数基于环境记录一种动态作用域链。
- 标志符查找: 当引擎试图解析一个标识符时,它会从当前的作用域开始沿着作用域链向上查找,直到找到该标识符的绑定为止。如果没有找到绑定,则可能会抛出引用错误。
- 标志符修改 : 可以通过声明环境记录来修改标识符的绑定值。但是,如果标识符被声明为只读(例如使用
const
),则不能修改其绑定值。 - 标志符删除 : 可以从声明环境记录中删除标识符的绑定。不过,在某些情况下(如使用
const
或let
声明的变量),删除标识符的绑定可能是不允许的
本章节你只需知道有这么几种环境记录,大致有什么作用即可。关于标志符的绑定,作用域链,标志符的查找,真正的操作者其实是执行上下文,后续的文章会有详细的讲解。
环境记录的分类
环境记录用来记录代码创建的标识符绑定,为什么说是标志符,而不是说申明呢?
比如如下的 err
也会建立绑定关系,但不算是申明吧,以及 arguments
也是类似。
javascript
javascript
复制代码
try{
throw new Error('err')
}catch(err){
}
代码运行,会根据具体的运行情况,创建不同的环境记录,分为如下几种类型:
英文名 | 中文 | 创建场景 |
---|---|---|
Declarative Environment Record | 申明环境记录 | 全局环境记录,模块环境记录,函数环境记录,对象环境记录之外创建的都是 申明环境记录。catch语句,块语句,for/in语句,for/of语句,特定条件的函数等等都会创建申明环境记录。 |
Function Environment Record | 函数环境记录 | 函数执行。申明环境记录的子类。 |
Module Environment Record | 模块环境记录 | 模块脚本。申明环境记录的子类。 |
Object Environment Record | 对象环境记录 | with语句和全局环境记录。 |
Global Environment Record | 全局环境记录 | Realm初始化。 |
函数环境记录 和 模块环境记录是申明环境记录的子类。
接下来会重点介绍一下 函数环境记录,对象环境记录,全局环境记录。 你只需大概了解环境记录的概念和基本存储结构就行。
环境记录的抽象方法(即全部环境记录都拥有的方法,了解即可)
即所有的环境记录都有对应的实现,可以看到都是和绑定关系关联的方法。
方法 | 说明 |
---|---|
HasBinding(N) | 环境记录中是否存在绑定N |
CreateMutableBinding(N, D) | 创建一个新的但未初始化的可变绑定。如果 D 为 true,绑定可能随后被删除。 |
CreateImmutableBinding(N, S) | 创建一个新的但未初始化的不可变绑定。如果 S 为 true,那么在初始化之后尝试设置它总是会抛出异常。 |
InitializeBinding(N, V) | 设置已存在但未初始化的绑定的值。 |
SetMutableBinding(N, V, S) | 设置已经存在的可变绑定的值。如果 S 为 true 且无法设置绑定,则引发 TypeError 异常。 |
GetBindingValue(N, S) | 环境记录返回已存在绑定的值。如果 S 为 true 且不存在绑定,则引发 ReferenceError 异常。如果绑定存在但未初始化,则抛 ReferenceError,与 S 的值无关。 |
DeleteBinding(N) | 从环境记录中删除绑定。如果存在 N 的绑定,删除绑定并返回 true。如果绑定存在但无法删除,则返回 false。如果绑定不存在,则返回 true。 |
HasThisBinding() | 是否存在this绑定。 |
HasSuperBinding() | 是否存在super绑定。 |
WithBaseObject() | 如果此环境记录与一个 with 语句相关联,则返回 with 对象。否则,返回未定义的。 |
Declarative Environment Record 申明环境记录
至于什么时候创建申明环境记录,这个不太好罗列,但是可以用排他法来表达,其他环境记录的创建场景都是非常清晰的,所以全局环境记录,模块环境记录,函数环境记录,对象环境记录之外创建的都是 申明环境记录。
比如经典 try catch 语句
javascript
javascript
复制代码
try {
throw new Err('err')
}catch(err){
let a = 'a'
debugger
}
当执行到 debugger 语句时,你可以访问 err 和 a 标志符,这两者其实是保存到不同的申明环境记录的。 这里 a 所在的环境记录 的(outerEnv) 外部环境记录 就是 err所在的申明环境记录, 因为访问是从内到外的。有点抽象,么事看关系图。

- 标志符的访问行为是执行上下文发起的,而且查找的起点大都是其 LexicalEnvironment 属性指向的环境记录,本示例就是
a
所在的申明环境记录 - OuterEnv 指向外部环境记录,形成链路,执行上下文就可以访问到链路上的标识符。
- 链路的终点是全局环境记录,这就是为什么无论在哪都能访问全局变量,全局对象。
接下来,要说的是函数环境记录 (Function Environment Record) 和 模块环境记录(Module Environment Records ) , 其都是申明环境记录的子类。
Function Environment Record 函数环境记录
申明环境记录的子类。函数环境记录是一个声明性环境记录,用于表示函数的顶级作用域, 注意了是顶级,因为如果函数里面包含了函数,其会生成新的 函数环境记录。
其有一些特有的属性,
[[ThisValue]]
: 可作为程序上的this[[ThisBindingStatus]]
: this的绑定状态[[FunctionObject]]
: 触发创建这个环境记录的函数对象,即日常编程中的function
。

以如下代码为例:
javascript
javascript
复制代码
"use strict"
function getResult(){
var varA = 1;
var varB = 2;
debugger
return varA + varB;
}
getResult();
当函数被执行时会创建一个函数环境记录,代码运行到debugger
时,环境记录的大致情况如下:


- FunctionObject 是函数对象,对应着开发者的 function 函数
- arguments 是函数内置的参数
当然环境记录是不能单独存在的,其有相关的执行上下文,此外前面提到,通过 OuterEnv 形成链路,也就是作用域链,后面会细说。 所以 其全貌如下

说明:
- 其实每个标志符后面对应的不直接是值,而是属性描述符/或者表示绑定关系的对象。这么写是为了直观,方便理解。
- 函数执行时,除了会生成函数环境记录,还会根据函数的严格模式,有无参数表达式等情况,创建不同数量的环境记录。
- 全局环境记录由 一个对象环境记录和一个申明环境记录组成。
Module Environment Records 模块环境记录
申明环境记录的子类。模块环境记录包含模块顶层声明的绑定。它还包含模块显式导入的绑定。它的[[ OuterEnv ]]是一个全球环境记录。
在编程上直接相关的就是 export
和 import
关键字了。 这个之后在深入的探究。
Object Environment Record 对象环境记录
对象环境记录用于定义 ECMAScript 元素(如 with 语句)的效果,这些元素将标识符绑定与某些对象的属性关联起来。
现在就很好理解 with
语句了吧, 因为对象环境记录把标识符绑定与某些对象的属性关联起来,所以可以看起来可以直接使用对象属性。
字段名 | 值 | 含义 |
---|---|---|
[[BindingObject]] | 一个Object | 对象环境记录绑定的对象。比如如下实例就是 obj。 |
[[IsWithEnvironment]] | Boolean值 | 是否为 with 语句创建。 true表示with语句,false全局环境(目前没看到其他处) |
比如如下实例 [[BindingObject]]
就是 obj
ini
javascript
复制代码
var obj = {name:1};
with(obj){
......
}
一起看看如下代码的对象环境记录
ini
javascript
复制代码
var obj = {name:1};
with(obj){
let a = 1;
debugger
console.log(name);
}
with语句会生成一个 对象环境记录,运行到 debugger
,其对应的环境记录大致如下
当然环境记录是不能单独存在的,其有相关的执行上下文,此外前面提到,通过 OuterEnv 形成链路,也就是作用域链,后面会细说。 所以 其全貌如下

说明
-
这里可以看到执行上下文的
LexicalEnvironment
首先指向的是申明环境记录,申明环境outerEnv 指向对象环境记录, 对象环境记录outerEnv指向全局环境记录。- 这是因为 with 对应的块语句 也会创建一个申明环境记录,用于保存 const/let/class等词法申明
-
全局环境记录 上也有一个对象环境记录,只不过
IsWithEnvironment
的值是false, 表示不是 with 语句创建的环境记录。
关于with
有一些小点,了解一下还是比较好:
- 非字符串的标志符(属性)不会被包含, 比如
Symbol
- 自己的和继承的属性都包含
- 不可枚举的属性会包含
javascript
javascript
复制代码
function createObj(){
var symbolP1 = Symbol.for('p1');
var obj = {
[symbolP1]: 'symbolP1',
objName: 'objName',
}
// p2 不可被枚举
Object.defineProperty(obj, "p2", {
enumerable: false,
value: 'p2'
});
return obj;
}
var obj = createObj();
with(obj){
console.log(objName); // objName
console.log(p2); // p2
// 原型上
console.log(constructor.name); // Object
// 期望的值是 symbolP1
console.log(Symbol.for('p1')); // Symbol(p1)
console.log(symbolP1); // Uncaught ReferenceError: symbolP1 is not defined
}
和 with
效果类似的其实还有特殊的全局对象上的属性和方法, 其实本质上也是用对象环境记录实现的。
Global Environment Record 全局环境记录
全局环境记录逻辑上是一个单独的记录,但在规范中被定义为复合结构,封装了一个对象环境记录 和一个声明性环境记录。
-
对象环境记录: 包含所有
- 内置全局的绑定 (比如NaN, globalThis, Date等等),
- 以及代码中包含的 FunctionDeclaration、 GeneratorDeclaration、 AsyncFunctionDeclaration、 AsyncGeneratorDeclaration
- 或 VariableStatement 引入的所有绑定。
-
申明环境记录 :
let
,const
,class
等申明的绑定关系。
现在大家知道了吧, 你是怎么访问到全局的变量或者方法的,归功于全局环境记录的对象环境记录。
配合一段代码来理解
ini
javascript
复制代码
function a(){};
async function asyncA(){};
var varA = 'varA';
let letA = 'letA';
const constA = 'constA';
class classA {};
debugger
当运行到 debugger时,关系图如下

- 执行上下文的 LexicalEnvironment 和 VariableEnvironment 都指向全局环境记录
- 对象环境记录的 BindingObject 就是 GlobalThisValue, 即 开发者眼中的 globalThis, 可以看到除了保存用户创建的全局变量,还有内置的全局属性或者方法,比如Date, Promise,encodeURI, NaN等等
- 全局顶层代码的 let/const/class 申明保存在申明环境记录
- 标志符查找时, 先从 申明环境记录 获取,然后从 对象环境记录 获取。
结合上图思考,为什么可以用如下两种方式访问 Date
ini
javascript
复制代码
var date = new Date();
var date2 = globalThis.Date();
提示:
LexicalEnvironment.ObjectRecord.BindingObject.properties.get("globalThis").Value === agent.runningExecutionContext.LexicalEnvironment.GlobalThisValue
再从全局环境记录的其他字段 来加强一下理解:
字段 | 值 | 含义 |
---|---|---|
[[ObjectRecord]] | 对象环境记录 | 其[[BindingObject]] 是全局对象(global object)。 它包含全局内置绑定以及关联领域的全局代码中的 FunctionDeclaration、 GeneratorDeclaration、 AsyncFunctionDeclaration、 AsyncGeneratorDeclaration 和 VariableDeclaration 绑定。 |
[[GlobalThisValue]] | 对象 | 全局环境的this。 |
[[DeclarativeRecord]] | 申明环境记录 | 包含关联领域代码的全局代码中的所有声明的绑定,但 FunctionDeclaration、 GeneratorDeclaration、 AsyncFunctionDeclaration、 AsyncGeneratorDeclaration 和 VariableDeclaration 绑定除外。 |
[[VarNames]] | 字符串 List | 各种声明绑定的字符串名称。 |
从表格对 对象环境记录 和 申明环境记录 的描述,可以得出:
- let , const 和 class 这类绑定关系是保存在 申明环境记录
- var 申明 和 各种函数申明,内置对象 的绑定关系保存在 对象环境记录
[[VarNames]]
出现的地方: 基本都是用于检查环境记录中是否存在某申明
检查是否有对应的申明。用于GlobalDeclarationInstantiation ( script, env ) 全局代码执行时检查申明。
全局环境记录的额外方法(了解即可)
方法 | 作用 |
---|---|
GetThisBinding() | 返回此环境记录的this绑定的值,通常是全局对象。 |
HasVarDeclaration (N) | 判断参数标识符N是否在此环境记录中有通过VariableDeclaration、FunctionDeclaration、GeneratorDeclaration、AsyncFunctionDeclaration或AsyncGeneratorDeclaration创建的绑定。 |
HasLexicalDeclaration (N) | 判断参数标识符N是否在此环境记录中有通过LexicalDeclaration(如let或const声明)或ClassDeclaration创建的绑定。 |
HasRestrictedGlobalProperty (N) | 判断参数是否为全局对象中不能被全局词法绑定覆盖的属性名。有一些全局对象的属性是内在定义的,它们不能被同名的变量或函数声明覆盖。例如,全局对象上的undefined 属性是一个系统保留的关键字,其值始终为 undefined ,且无法通过var声明或函数声明来重新定义或隐藏它。 |
CanDeclareGlobalVar (N) | 查在当前全局环境记录中,是否允许为给定的变量名N创建一个新的全局变量绑定 |
CanDeclareGlobalFunction (N) | 检测在当前全局环境记录中,是否能够成功为给定的函数名N创建一个新的全局函数绑定 |
CreateGlobalVarBinding(N, D) | 在全局环境记录的 对象环境记录 中创建并初始化一个全局var绑定,将其值设为undefined。这个绑定是可变的(mutable binding)。与之对应的全局对象属性将会具有适合var声明的属性值。字符串值N是绑定的名称。如果布尔参数D为true,则表示该绑定可以被删除。 |
CreateGlobalFunctionBinding(N, V, D) | 在全局环境记录的 对象环境记录 组件中创建并初始化全局函数绑定。该绑定是可变绑定。对应的全局对象属性将具有适合函数声明的属性值。字符串值N是绑定的名称。V是初始化值。如果布尔参数D为true,则该绑定可以被删除。此方法逻辑上等同于先调用CreateMutableBinding再调用SetMutableBinding,但允许函数声明获得特殊处理。 |
GetBindingValue ( N, S )
取标志符N的值。 其基本逻辑是先从 申明环境记录 获取,然后从 对象环境记录 获取。

这种逻辑,就能解释如下的代码:
javascript
javascript
复制代码
let clearTimeout = 100; // let, const的绑定关系保存在申明环境记录
console.log(clearTimeout) // 先从申明环境记录中查找 结果:100
console.log(globalThis.clearTimeout) // 对象环境记录 ƒ clearTimeout() { [native code] }
console.log(clearTimeout)
的寻得路径。

console.log(globalThis.clearTimeout)
的寻得路径, 是先找到 globalThis,然后取属性 clearTimeout。

环境上的操作
GetIdentifierReference ( env, name, strict )
从环境记录上查找 标志符符name
的的引用记录, 引用记录是一种内部数据结构,用于描述如何访问或修改一个变量或属性。

环境记录上有着各种绑定关系,
执行上下文(后续文章会细说)是通过 ResolveBinding ( name [ , env ] ) 找到对应的引用记录, 其底层是通过环境记录的 9.1.2.1 GetIdentifierReference ( env, name, strict ) 通过标志符获取引用记录。

- env: 是环境记录
- name: 标志符,比如
var a = 10
中的a
就是标志符。 - strcit: 是否是严格模式
操作步骤
- 如果环境记录为空,返回不可达的引用记录。
- 如果有绑定,返回引用记录
- 从环境记录的
[[outerEnv]]
即外围的环境记录继续查找
scss
javascript
复制代码
function log(){
console.log(aaaa)
}
log();
其执行上下文,环境记录,标志符关系图如下

这里执行 console.log(aaaa)
的时候,需要查找 标志符aaaa
是否存在引用记录。
这里有两个环境记录:
- log函数的环境记录,
[[outEnv]]
全局环境记录 - 全局环境记录
- log函数环境记录,没有找到,查找上层环境记录
- 全局环境记录先找申明环境记录,再找对象环境记录,都没找到, 继续查找上层, 这时候 OuterEnv 已经为空
- env为空,所以返回了一个引用记录,其
[[base]]
的值为unresolvable
下图演示了查找的路径,最外层全局环境记录会发生两次查找。

这里注意了, 查找不到标志符的引用关系,也会生成一个 引用记录 。 这一点很重要。 这就能很好的解释之后的 typeof aaaaaa 不抛出异常的行为。
实际运行上面的代码是会抛出异常的

都有了引用记录,咋还能出异常呢? 这是因为 console.log(aaaa)
要打印其值,所以会发生
[[getValue]]
取值的行为,如果引用不可达,就抛出ReferenceError

下面的方法都是初始化更各种环境记录,一般情况下都需要传入外部的环境记录。 也有例外:
- 全局环境记录除外,其没有外部环境。
- 函数环境记录环境也是例外,因为函数必然存在于某个环境记录中,可以通过 函数的
[[Environment]
获取到其所在的环境记录。
NewDeclarativeEnvironment ( E )
创建一个新的申明环境记录。参数 E 是一个环境记录, 会把新的环境记录的 [[outerEnv]]
指向 E。

NewObjectEnvironment ( O, W, E )
创建一个新的对象环境记录。 流程:新建一个环境记录,并对[[BindingObject]]
,[[IsWithEnvironment]]
,[[OuterEnv]]
赋值。
- O:被绑定的对象。 赋值给新对象环境记录的
[[BindingObject]]
。 - W:是不是with语句创建。赋值给新对象环境记录的
[[IsWithEnvironment]]
。
with语句和全局环境都有这对象记录环境,那怎么识别的呢? 就是 [IsWithEnvironment]]
, true表示 with语句,false是全局环境。
- E: 外部的环境记录。 赋值给新对象环境记录的
[[OuterEnv]]

非with语句创建对象环境记录。

with语句创建对象环境记录。

NewFunctionEnvironment ( F, newTarget )
创建一个函数环境记录。函数环境记录是什么时候被创建的呢? 是函数准备被调用的时候创建的。更多细节可以参见 PrepareForOrdinaryCall。
这个newTarget 又是什么呢? 这个你先得了解一点反射,
引用MDN Reflect.construct(target, argumentsList[, newTarget]) 的示例。newTarget 作为新创建对象的原型对象的 constructor 属性。
javascript
javascript
复制代码
function OneClass() {
this.name = 'one';
}
function OtherClass() {
this.name = 'other';
}
// 创建一个对象:
var obj1 = Reflect.construct(OneClass, [], OtherClass);
// 与上述方法等效:
var obj2 = Object.create(OtherClass.prototype);
OneClass.apply(obj2, []);
console.log(obj1.name); // 'one'
console.log(obj2.name); // 'one'
console.log(obj1 instanceof OneClass); // false
console.log(obj2 instanceof OneClass); // false
console.log(obj1 instanceof OtherClass); // true
console.log(obj2 instanceof OtherClass); // true
// 原型对象的构造函数
Object.getPrototypeOf(obj1).constructor // OtherClass
Reflect.construct调用的链路, 可以看到 newTarget参数一路狂奔。
Reflect.construct ( target, argumentsList [ , newTarget ] )
=> 7.3.15 Construct ( F [ , argumentsList [ , newTarget ] ] )
=> 10.2.2 [[Construct]] ( argumentsList, newTarget )
=> 10.2.1.1 PrepareForOrdinaryCall ( F, newTarget )
=> 9.1.2.4 NewFunctionEnvironment ( F, newTarget )
整个执行逻辑如下

NewGlobalEnvironment ( G, thisValue )
创建全局环境记录。内部逻辑是创建非with的对象环境记录和申明环境记录。

疑问
全局环境记录的变量 不能删除? 哪来看出来?