结合 JavaScript 规范来谈谈 Execution Contexts 与 Lexical Environments

结合 JavaScript 规范来谈谈 Execution Contexts 与 Lexical Environments

  • 引言
  • 作用域
  • Execution Context & Execution Context Stack
  • Lexical Environments
    • Environment Record
    • LexicalEnvironment 与 VariableEnvironment
    • 示例
    • VO & AO
  • 参考资料

引言

重新回头看看 JavaScript 的内容,做一做查漏补缺,却相关的资料繁多:有些谈论着什么 Variable Object、Activation Object,有些又在谈论着 LexicalEnvironment 与 VariableEnvironment。所讲述的内容虽然大抵类似,但是一番东西却变着出来两番叫法,总是叫人怀疑。想来还是该寻根溯源,到 ECMAScript 规范来找寻答案。

作用域

JavaScript 中的作用域是 词法作用域,与动态作用域相对,其定义在词法阶段,函数的作用域基于函数创建的位置。

词法作用域意味着作用域时由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
– YDKjs

Execution Context & Execution Context Stack

Execution context(执行上下文)是什么?先来看看规范中给出的定义:

An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.
– ECMAScript® 2019 Language Specification

简单来理解,JavaScript 引擎会为可执行的代码创建对应的 execution context,这一“可执行的代码”可能是全局代码或是函数代码等。

Execution context stack 用于追踪、存储这些 execution contexts。一如其名,execution context stack 是一个后进先出(LIFO)的栈结构。在栈顶上永远是 running execution context,当控制从当前 context 对应的代码转移到另一段代码时,相应的 execution context 将被创建,新的 execution context 会被压入栈顶。同时,栈底总会有一个 global execution context。
下图是一个简单的 execution context stack 示例:

结合 JavaScript 规范来谈谈 Execution Contexts 与 Lexical Environments_第1张图片
图片来自 Understanding Execution Context and Execution Stack in Javascript

值得注意的是,虽然 execution context stack 是栈结构,但这并不意味着 JS 引擎只能严格按照 LIFO 的方式处理 context,有些 ECMAScript 的特性会导致其违反 LIFO 的规则:

Transition of the running execution context status among execution contexts usually occurs in stack-like last-in/first-out manner. However, some ECMAScript features require non-LIFO transitions of the running execution context.
– ECMAScript® 2019 Language Specification

Execution context 中包含多个状态:code evaluation state、Function、Realm、ScriptOrModule 等,但最值得我们关注的是其中的 LexicalEnvironment 以及 VariableEnvironment。这两者均为 Lexical Environment ,只不过针对的声明种类不同。

Lexical Environments

首先来看看 Lexical Environment 在规范中的定义:

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® 2019 Language Specification

简单来讲 Lexical environment 提供了 标识符(identifier)与变量(variable)的映射
Lexical environment 包含两个部分:

  • Environment Record:记录了在该 lexical environment 中被创建的标识符与变量的映射;
  • Reference to an outer Lexical Environment:lexical environment 对外部 lexical environment 的引用构成了嵌套结构,当然这一引用可能为空。

通过 lexical environment 所提供的映射关系,JS 引擎在执行过程中便可以沿着嵌套结构寻找所需的值。

根据关联代码的不同 lexical environment 可以分为三种:

  • Global Environment:对于没有外部 lexical environment 的代码(reference 为 null),其所对应 lexical environment 即为 global environment,其可能会将该代码关联的 global object 中所有属性添加到 environment record 中,并可以在执行过程中添加属性、修改初始属性值;
  • Module Environment:Module environment 包含了模块顶层的声明以及导入的声明,其外部 lexical environment 为 global environment;
  • Function Environment:Function environment 对应于 JS 中的函数,其会建立 this 的绑定以及必要的 super 方法的绑定。

Environment Record

Environment Record 分为五种:

  • Declarative Environment Records:记录 varconstletclassimportfunction 等声明;
  • Object Environment Records:与某一对象相绑定,其会记录该对象中具有 string 标识符的属性,非 string 标识符的属性不会被记录。Object environment records 为 with 语句所创建;
  • Global Environment Records:类似于 declarative environment records 与 object environment records 的结合,在包含顶层声明的同时还包含 global object 的属性;
  • Function Environment Records:为 declarative environment records 的子类,用于函数的顶层,如果该函数非箭头函数则提供 this 的绑定,如果该函数还引用了 super 则提供 super 方法的绑定;
  • Module Environment Records:为 declarative environment records 的子类,用于 ES module 的顶层,除去常量和变量的声明,还包含不可变的 import 的绑定,该绑定提供了到另一 environment records 的间接访问。

其中,declarative environment records 与 object environment records 为两种主要的 environment record。
在我查阅的一些资料(例如 Understanding Execution Context and Execution Stack in Javascript 以及 What really is a declarative environment record and how does it differ from an activation object?)中, global environment record 似乎被视作为等同于或者 object environment record 的一种,但就我阅读规范的过程中发现并非如此:

A global Environment Record is used to represent the outer most scope that is shared by all of the ECMAScript Script elements that are processed in a common realm. A global Environment Record provides the bindings for built-in globals (clause 18), properties of the global object, and for all top-level declarations (13.2.8, 13.2.10) that occur within a Script.
A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record. The object Environment Record has as its base object the global object of the associated Realm Record.
– ECMAScript® 2019 Language Specification

规范中明确说明 global environment record 是两种 environment record 的组合,并同时包含了 [[DeclarativeRecord]] 以及 [[ObjectRecord]] 两个 field,分别对应 declarative environment record 与 object environment record。

LexicalEnvironment 与 VariableEnvironment

之前章节提到 LexicalEnvironmentVariableEnvironment 均为 Lexical Environment 的实例,均包含 environment record 以及对外部 environment record 的引用,两者的区别在于:LexicalEnvironment 中记录 letconst 等声明,VariableEnvironment 则仅处理 VariableStatementsvar 声明。

示例

为了更好的帮助读者理解,这里给出一个 lexical environment 的演示例子(其中的一些属性以及属性名仅为了便于理解,与实现无关)。
首先是 JS 源码:

// global environment
let a = 1;
const b = 2;
function f(arg1) {
  let c = arg1;
  var d = 3;
}
f(a);

代码执行时,JS 引擎会为其创建 execution context:

GEC = {
  lexicalEnvironment: {
    environmentRecord: {
      type: "Global",
      declarativeRecord: {
        type: "Declarative",
        a: <uninitialized>,
      	b: <uninitialized>,
      	f: <function>
      },
      objectRecord: {
      	type: "Object",
      	Infinity: +,
      	isFinite: <function>
      	...balabala
      }
    },
    refToOuter: null
  }
}

顶层代码 execution context 对应的 global environment record,包含 declarative 以及 object 两个部分。其中,declarative 部分声明的量将在执行阶段被赋值,object 部分则包含了全局对象中的属性。

函数 f 被调用时,JS 引擎也会为其创建一个 execution context:

FEC = {
  lexicalEnvironment: {
    environmentRecord: {
      type: "Function",
      Arguments: {0: 1, length: 1},
      c: <uninitialized>,
      this: <Global Object>
    },
    refToOuter: GEC // global execution context
  },
  variableEnvironment: {
    environmentRecord: {
      type: "Function",
      d: undefined,
      this: <Global Object>
    },
    refToOuter: GEC // global execution context
  }
}

需要注意的是,lexicalEnvironment 中声明的量与 variableEnvironment 中声明的量在未执行时所处的 初始状态是不同的,前者为未初始化状态,后者为 undefined。这也能够解释为什么在 let 或者 const 语句执行前访问对应变量会导致报错,而 var 语句执行前访问变量会得到 undefined

VO & AO

现在已经到了2020年了,ES2018、ES2019 都翻着法儿抖落出新花样了,却还是有许多资料乐此不疲的谈论着 VO(Variable Object) 以及 AO(Activation Object)的资料,讨论 Lexical Environments 却数目寥寥。
事实上,VO 与 AO 均为 ES3 规范中所包含的内容,自从 ES5 规范起至 ES2019 规范,便只有 Lexical Environments ,再也寻不见 VO 与 AO 的影子,所以还是得与时俱进加强学习啊。

参考资料

  1. ECMAScript® 2019 Language Specification
  2. YDKjs
  3. Understanding Execution Context and Execution Stack in Javascript
  4. 冴羽的博客

你可能感兴趣的:(前端杂物筐,JavaScript)