ECMAScript 环境记录

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

环境记录是一种规范类型,环境记录通常与 ECMAScript 代码的某些特定语法结构相关联,例如 函数申明(FunctionDeclaration)、 块语句(BlockStatement) 等等。每次计算这些代码时,都会创建一个新的 Environment Record 来记录代码创建的标识符绑定。

每个环境记录都有一个[[ OuterEnv ]]字段,该字段要么为 null,要么是对外部环境记录的引用。

环境记录 是纯粹的规范机制,不需要与 ECMAScript 实现的任何特定构件相对应。

作用

  1. 标识符绑定: 声明环境记录负责维护标识符与其绑定的值之间的映射关系。当一个标识符在一个作用域内被声明时,相应的绑定会在声明环境记录中创建。
  2. 作用域链: 当创建一个新的声明环境记录时,它通常会关联一个外部环境记录,形成一个作用域链。这使得内部作用域能够访问外部作用域中的标识符绑定。
  3. 闭包: 从协议上来看,闭包本质是函数基于环境记录一种动态作用域链。
  4. 标志符查找: 当引擎试图解析一个标识符时,它会从当前的作用域开始沿着作用域链向上查找,直到找到该标识符的绑定为止。如果没有找到绑定,则可能会抛出引用错误。
  5. 标志符修改 : 可以通过声明环境记录来修改标识符的绑定值。但是,如果标识符被声明为只读(例如使用 const),则不能修改其绑定值。
  6. 标志符删除 : 可以从声明环境记录中删除标识符的绑定。不过,在某些情况下(如使用 constlet 声明的变量),删除标识符的绑定可能是不允许的

本章节你只需知道有这么几种环境记录,大致有什么作用即可。关于标志符的绑定,作用域链,标志符的查找,真正的操作者其实是执行上下文,后续的文章会有详细的讲解。

环境记录的分类

环境记录用来记录代码创建的标识符绑定,为什么说是标志符,而不是说申明呢?

比如如下的 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 形成链路,也就是作用域链,后面会细说。 所以 其全貌如下

说明:

  1. 其实每个标志符后面对应的不直接是值,而是属性描述符/或者表示绑定关系的对象。这么写是为了直观,方便理解。
  2. 函数执行时,除了会生成函数环境记录,还会根据函数的严格模式,有无参数表达式等情况,创建不同数量的环境记录。
  3. 全局环境记录由 一个对象环境记录和一个申明环境记录组成。

Module Environment Records 模块环境记录

申明环境记录的子类。模块环境记录包含模块顶层声明的绑定。它还包含模块显式导入的绑定。它的[[ OuterEnv ]]是一个全球环境记录。

在编程上直接相关的就是 exportimport关键字了。 这个之后在深入的探究。

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有一些小点,了解一下还是比较好:

  1. 非字符串的标志符(属性)不会被包含, 比如 Symbol
  2. 自己的和继承的属性都包含
  3. 不可枚举的属性会包含
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, constclass 等申明的绑定关系。

现在大家知道了吧, 你是怎么访问到全局的变量或者方法的,归功于全局环境记录的对象环境记录。

配合一段代码来理解

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: 是否是严格模式

操作步骤

  1. 如果环境记录为空,返回不可达的引用记录。
  2. 如果有绑定,返回引用记录
  3. 从环境记录的[[outerEnv]] 即外围的环境记录继续查找
scss 复制代码
javascript
复制代码
function log(){
  console.log(aaaa)
}

log();

其执行上下文,环境记录,标志符关系图如下

这里执行 console.log(aaaa)的时候,需要查找 标志符aaaa 是否存在引用记录。

这里有两个环境记录:

  • log函数的环境记录, [[outEnv]]全局环境记录
  • 全局环境记录
  1. log函数环境记录,没有找到,查找上层环境记录
  2. 全局环境记录先找申明环境记录,再找对象环境记录,都没找到, 继续查找上层, 这时候 OuterEnv 已经为空
  3. 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的对象环境记录和申明环境记录。

疑问

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

相关推荐
0xHashlet几秒前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝1 分钟前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大3 分钟前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端
前端涂涂4 分钟前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端
凌叁儿4 分钟前
从零开始搭建Django博客③--前端界面实现
前端·python·django
博弈美业系统Java源码4 分钟前
连锁美业管理系统「数据分析」的重要作用分析︳博弈美业系统疗愈系统分享
java·大数据·前端·后端·创业创新
木子李i5 分钟前
Cesium离线使用和部署地图影像
前端·cesium
本本啊7 分钟前
node 启动本地应用程序并设置窗口大小和屏幕显示位置
前端·node.js
echoVic8 分钟前
PixiJS 源码揭秘 - 8. 插件机制深度解析
前端·源码·数据可视化
Vincent_Chen9 分钟前
Vue 2 源码解读指南
前端