重新回头看看 JavaScript 的内容,做一做查漏补缺,却相关的资料繁多:有些谈论着什么 Variable Object、Activation Object,有些又在谈论着 LexicalEnvironment 与 VariableEnvironment。所讲述的内容虽然大抵类似,但是一番东西却变着出来两番叫法,总是叫人怀疑。想来还是该寻根溯源,到 ECMAScript 规范来找寻答案。
JavaScript 中的作用域是 词法作用域,与动态作用域相对,其定义在词法阶段,函数的作用域基于函数创建的位置。
词法作用域意味着作用域时由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
– YDKjs
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 示例:
图片来自 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 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 包含两个部分:
通过 lexical environment 所提供的映射关系,JS 引擎在执行过程中便可以沿着嵌套结构寻找所需的值。
根据关联代码的不同 lexical environment 可以分为三种:
this
的绑定以及必要的 super
方法的绑定。Environment Record 分为五种:
var
、const
、let
、class
、import
、function
等声明;with
语句所创建;this
的绑定,如果该函数还引用了 super
则提供 super
方法的绑定;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 均为 Lexical Environment 的实例,均包含 environment record 以及对外部 environment record 的引用,两者的区别在于:LexicalEnvironment 中记录 let
、const
等声明,VariableEnvironment 则仅处理 VariableStatements
即 var
声明。
为了更好的帮助读者理解,这里给出一个 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
。
现在已经到了2020年了,ES2018、ES2019 都翻着法儿抖落出新花样了,却还是有许多资料乐此不疲的谈论着 VO(Variable Object) 以及 AO(Activation Object)的资料,讨论 Lexical Environments 却数目寥寥。
事实上,VO 与 AO 均为 ES3 规范中所包含的内容,自从 ES5 规范起至 ES2019 规范,便只有 Lexical Environments ,再也寻不见 VO 与 AO 的影子,所以还是得与时俱进加强学习啊。