Skip to main content

深入理解:ECMAScript 6中的执行上下文

· 约19分钟
Proca

导语

执行上下文(Execution Context)是JavaScript引擎解析可执行代码片段时创造的一种环境。在 深入理解:ECMAScript 3中的执行上下文 中,我们介绍到 ECMAScript 3 规范(下文简称 ES3 规范) 下的执行上下文由变量对象或活动对象、作用域链、调用者信息三部分内容组成,但同时我们也提到 ES3 规范 是古老的。在相较更新的 ECMAScript 6 规范 (下文简称 ES6 规范) 中, letconst 关键字的引入、类( class )在语言层面上的被支持,以及其他种种新特性,都让 ES3 规范 下的执行上下文显得无能为力。因此, ES6 规范 在引入新特性的同时,也要对执行上下文的相关机制做出调整。

本文将立足于 ES6 规范,介绍其执行上下文的组成以及其在实践中的运用。

ECMAScript 6中执行上下文的组成

在认识 ES6 规范 中执行上下文的组成前,我们先来复习一下 ES3 规范 中执行上下文的伪代码表示:

executionContext = {
//变量对象或活动对象
[Variable Object | Activation Object]: {
arguments,
variables: [...],
funcions: [...]
},

//作用域链
scopeChain: VO|AO.concat([[scope]]),

//调用者信息
thisValue: context object
}

相较 ES3 规范 中简单的执行上下文组成,ES6 规范 中的执行上下文稍显复杂,其伪代码如下:

executionContext = {

codeEvaluationState,

Function,

Realm,

LexicalEnvironment, //划重点

VariableEnvironment,

Generator

}

天呐!看起来跟 ES3 规范 的执行上下文没有任何类似的地方!我们学 ES3 规范 的执行上下文是不是白学了?

当然不是,很快你就会知道 ES3 规范ES6 规范 在执行上下文内容上的联系。现在先让我们逐一了解以上伪代码中的各个属性。

code evaluation state

code evaluation state refers to any state needed to perform, suspend, and resume evaluation of the code associated with this execution context.

根据 ES6 规范code evaluation state 代表当前执行上下文所对应的可执行代码的状态,常见的状态有:

  1. 执行(perform)

  2. 挂起(suspend)

还记得我们在深入理解:ECMAScript 3中的执行上下文 中介绍的执行上下文栈(ECS)吗?在 ES6 规范 中当然也存在这样的执行上下文栈,并且 code evaluation state 对当前执行上下文的状态标记,使得ECS中堆栈式的后进先出(stack-like last-in/first-out)机制成为可能。如下图所示:

codeEvaluationStatus

Function

ES6 规范 中的 Function 代表当前正在分析的函数对象,如果当前执行的是脚本(Script)或模块(Module)中的代码,那么执行上下文中 Function 的值就是 null

Realm

注意:这里的 Realm 并不指Realm数据库,而是ECMAScript规范中的一种抽象概念。

Realm 意为“领域”。在代码执行之前,所有的ECMAScript代码都必须和一个Realm关联。一个Realm由一组内部对象、一个ECMAScript全局环境以及在此全局环境作用域中加载的ECMAScript代码、关联的状态与资源共同组成。

根据以上描述及 ES6 规范 中的其他细节,我们可以知道 RealmRealm Record 的形式表示。该记录(Record)由以下字段以及字段对应的值组成:

字段
[[intrinsics]]当前领域的内部固有对象(规范上,全局对象的属性和方法基本由此而来)
[[globalThis]]当前领域的全局对象
[[globalEnv]]当前领域的全局环境
[[templateMap]]对于模版对象的规范(使用模版对象时会优先在Realm中查找)

实际上,Realm 只是一个抽象术语。在浏览器环境中,一个 window 是一个 Realm,一个 frame 也是一个 Realm,一个 web worker 也可以是一个 Realm。每个 Realm 都拥有自己的全局变量,而不同 Realm 间的对象继承将受到限制

简单起见,你完全可以将 Realm(领域)理解为 Scope(作用域)

Lexical Environment

我们在深入理解:ECMAScript 3中的执行上下文 中提到一个概念:变量对象(VO),事实上,这个对象是一个规范对象(Specification Object),只存在于语言规范的层面,我们无法直接在实际代码中获取这个对象,并对其进行操作。

类似地,词法环境(Lexical Environment)也是这样的一个规范对象。在了解了该对象的内容后,你会发现它与变量对象以及作用域链的组成具有一定的相似性。

根据规范,词法环境(Lexical Environment)拥有三种类型:

  1. global environment,全局环境
  2. module environment,模块环境
  3. function environment,函数环境

你可能已经注意到了,在 ES3 规范 中,我们对执行上下文分类,而在 ES6 规范 中,不同分类之间的最大区别就在于词法环境

不同的类型的词法环境在内容上具有差别,为了了解这些差别,我们需要先了解词法环境的内容。

同样根据规范,词法环境(Lexical Environment)的内容由两部分组成:

  1. 环境记录项(Environment Record)
  2. 外部词法环境 的引用(outer)

lexicalenvironment

环境记录项是一个记录所有局部变量作为其属性的规范对象,同时该对象也会记录调用者信息,即 this 的值。一个变量只是环境记录项这个对象的一个属性,“获取或修改变量”意味着“获取或修改环境记录项的一个属性”。

在规范中,环境记录项被抽象成一个简单的面向对象的层次结构(object-oriented hierarchy),也就是被看作为一个抽象类(class)。并且,该抽象类拥有三个子类(subclass)

说人话,就是环境记录项被分为三种类型:声明式环境记录项(declarative environment records)、对象式环境记录项(Object environment records)以及全局环境记录项(global environment records)。

接下来,让我们逐一了解这三种类型的环境记录项。

声明式环境记录项:

声明式环境记录项(declarative environment records)记录了作用范围内的变量以及函数,包括 constletclassmoduleimportfunction 的声明。可以理解为记录了变量名及其对应值的键值对。

例如,对于以下代码:

let foo = 2;
function bar() {}

其对应的声明式环境记录项长这样:

property        value
------------------------
foo 42
bar <function object>

注意:为简单起见,以上仅展示了环境记录项中记录的“键值对”。在规范中,所有类型的环境记录器还拥有多种方法,这些方法同样是语言规范层面的,被称为规范方法(specification methods)

实际上,环境记录项的类型不止我们上面提到的三种,其还包括另外两种:函数环境记录项(Function environment records)以及模块环境记录项(module environment records)。之所以在这里才提出它们,是因为在规范中,它们都属于声明式环境记录项(declarative environment records)的子类(subclass)。

对于函数环境记录项(Function environment records),它用于表示函数的顶层作用域,并且会为非箭头函数提供 this 绑定(由规范方法完成)。同时,它还会提供 super 关键字,为 ES6 规范 中的提供相应支持。

对于模块环境记录项(module environment records),它内部记录的是ES6 规范 的 Module 环境中的变量,除了支持 声明式环境记录项 内含的规范方法外,还为模块的导入提供了规范方法

对象式环境记录项:

对象式环境记录器记录了其绑定对象中的属性名称以及对应值(键值对)。

对于以下代码:

let obj = {
name: 'Proca',
age: 18
}

对应的对象式环境记录器如下:

property        value
------------------------
name 'Proca'
age 18

全局环境记录项:

注意:虽然我们把全局环境记录项单独拎出来介绍,与声明式环境记录项以及对象式环境记录项并列,但实际上它是一个复合记录项。在规范中,全局环境记录项是一个封装了声明式环境记录项以及对象式环境记录项的复合记录项

ES3 规范 中,全局执行上下文的变量对象是全局对象,而在 ES6 规范 中,全局词法环境的环境记录项就是全局环境记录项。在全局环境记录项中,对象环境记录项以全局对象作为基本对象,并记录了全局代码(global code)中的 FunctionDeclarationGeneratorDeclaration,以及 VariableStatement(一般是通过 var 关键字的声明)。声明式环境记录项则记录了没有在对象环境记录项中的其他声明,当然,这些声明也要求是出现在全局代码中的。

记不住那么多也没关系!

在上面,我们介绍了词法环境中的环境记录项,初学者可能会有些晕。事实上,在实际使用中,把环境记录项理解为 ES3 规范变量对象足矣。

outer

接下来我们来介绍词法环境中的另一组成:outer

如前所述,outer 是对外部词法环境的引用。在不同类型的词法环境中,outer 的指向可能不同。如下表:

词法环境类型outer的指向
全局词法环境null
模块词法环境全局词法环境
函数词法环境逻辑上将该函数词法环境包围的词法环境

不同的词法环境之间通过outer连接在了一起,形成了 深入理解:ECMAScript 3中的执行上下文 中介绍的作用域链结构,变量在不同词法环境中的共享便成为可能。

如我们刚才看到的那幅图:

lexicalenvironment

以上的三个不同的词法环境通过outer连接在了一起,形成了一条作用域链。

Variable Environment

通过上面的学习,我们知道词法环境的环境记录项可以记录通过 letconst 等关键字声明的变量。而通过 var 关键字声明的变量,需要在变量环境(Variable Environment)中记录。

在规范中,在执行上下文被创建时,词法环境与变量环境具有相同的值。随后,变量环境记录的是通过 var 关键字的声明,而其他声明则由词法环境记录。

为什么在规范中要将通过 var 关键字声明的变量与通过其他关键字(如 let )声明的变量分开在两个系统中记录呢?我们在学习 varlet 的区别时知道:通过 var 关键字声明的变量具有变量提升(Hoisting)的特性。

对于以下代码:

console.log(a);
console.log(b);

var a = 1; //(*)
let b = 1; //(**)

输出结果如下:

undefined
Uncaught ReferenceError: b is not defined

可以看到,通过 var 关键字声明的变量 a ,在代码分析到声明语句(即(*)行)前可以被使用,而通过 let 关键字声明的则不行。

上述代码与以下代码等价。

var a;
console.log(a);
console.log(b);

a = 1; //(*)
let b = 1; //(**)

我们把这种特性称为变量提升

要在规范中实现这种特性非常简单:如前所述,只要划分好两套系统,各自记录对应的变量。

对于记录在词法环境中的变量,在正式声明(如上述代码的(**)行)之前,将变量的值设置为 <uninitialized> :这是一种特殊的内部状态,意味着引擎知道该变量,但是不能对该变量进行引用(否则抛出错误),就像该变量不存在一样。

对于记录在变量环境中的变量,变量声明的初始化立即完成。于是,在正式声明之前,变量的值就已经是 undefined 了。

Generator

执行上下文中的 Generator 内容相对简单,在规范中也只是一笔带过:记录当前执行上下文中正在分析的迭代器对象。

关于迭代器的内容,参阅:Generator

内容小结

现在,我们对 ES6 规范 中执行上下文的组成有了比较深入的了解。让我们不厌其烦地再来复习一下:

executionContext = {

codeEvaluationState, //记录当前执行上下文的状态,决定执行上下文在执行上下文栈中何去何从

Function, //记录当前执行上下文分析的函数对象

Realm, //记录当前的领域(如浏览器环境下的 window )

LexicalEnvironment, //记录当前环境下的各种变量、函数、对象,以及对外部词法环境的引用

VariableEnvironment, //记录当前环境下通过 var 关键字声明的变量

Generator //记录当前执行上下文正在分析的迭代器对象

}

进一步加深理解

为了进一步加深对执行上下文的理解以及提高在实践中运用我们以上所学知识的能力,请观察以下代码:

var username = 'John';
let age = 18;

function userInfo(username, age) {
console.log("username: ", username, "age: ", age);
}

userInfo(username, age); //(*)

在实践中我们很少会关注执行上下文中的 code evaluation stateFunction 以及 Realm。我们关注的往往是 Lexical EnvironmentVariable Environment 以及 outer 的指向。因此,让我们从 实践 的角度对以上代码进行分析。

  1. 程序启动,全局执行上下文被创建,并被压入执行上下文栈栈顶,随着代码分析位置的变化,执行上下文的内容也发生变化。在引擎即将分析到(*)行时,该执行上下文的简易表示如下图:

firstStep

  1. 引擎分析到(*)行,函数执行上下文被创建,并被压入执行上下文栈栈顶,当(*)行开始执行,该执行上下文的表示如下图:

secondStep

其余步骤与 ES3 规范 中执行上下文的生命周期类似,在此不再赘述。