在 ECMAScript 中,执行上下文是一种用于跟踪代码执行状态的抽象规范。
每一个函数调用,都会创建一个新的执行上下文。函数调用栈用于管理所有的执行上下文。在任何时候,只能有一个执行上下文正在执行,我们可以称之为运行时上下文「the running execution context」。
运行时上下文始终处于栈顶。当代码执行过程中,有新的函数调用,新的执行上下文会被创建,入栈,并且成为新的运行时上下文。
组成
code evaluation state
执行上下文处于栈顶时,属于执行状态,若执行过程中有新的函数调用,则需要处于挂起状态,新的执行上下文执行完毕后,恢复为执行,code evaluation state 用于记录并控制上下文进入不同的状态中。
Function
function object 函数对象
普通对线包含 [[HasProperty]], [[GetOwnProperty]] 等内部方法,函数对线额外包含[[Call]] 与 [[Construct]] 两个内部方法
Call 表示函数可以被其他对象调用,Construct 表示函数可以被当成构造函数用于创建对象。
当前执行上下文处于栈顶时,其对应的 Function Object 可以称为 active function object。
Realm
资源域。需要注意的是这个域也表示范围,但是它与作用域是不同的概念。它表示的是当前代码的运行环境范围。例如,两个不同的页面,对应两个不同的域。一个页面中,包含一个 iframe 标签,也表示他们是不同的两个域。
一个 Realm 域由一个 Realm 记录对象呈现,具体包含如下内部属性:
- [[Intrinsics]] 该属性对应所有的内置对象,例如 Map,JSON等,以及一些全局方法 isNaN,具体可查看
- [[GlobalObject]] 全局对象
- [[GlobalEnv]] 全局环境记录
- [[TemplateMap]] 模板对象列表
- [[HostDefined]] 保留供主机使用的字段,表示与领域相关联的其他信息,通常为 undefined
LexicalEnvironment
词法环境,具体表现为一个环境记录对象,let/const会解析到该环境
VariableEnvironment
变量环境,var 声明的变量标识符,将会解析到该环境记录对象中。
注意
词法环境与变量环境,都是词法环境对象,并且在初始化时,他们具备相同的值。之所以会在一个执行上下文中,同时存在功能类似的两个环境记录对象,原因在于 let/const 与 var 的不同。这是历史原因导致的。在后续的内容中,词法环境和变量环境统称为词法环境。
ScriptOrModule
对应 script 标签或者模块。当代码处于其他环境时,该值为 null
环境记录对象
环境记录对象是执行上下文的重要组成部分。它用于根据代码的词法嵌套结构来绑定标识符与特定变量与函数之间的关联关系。因此每一个环境记录对象,都对应一些特定的语法结构。例如函数声明,try catch等。
通俗来说,环境记录就是用于收集各种变量声明,函数声明等。
除此之外,每一个环境记录对象都有一个内部字段 [[OuterEnv]],用于指向外部环境记录。
与执行上下文一样,环境记录是纯粹的规范机制,无法从外部访问或者操作这些值。
函数环境记录
函数环境记录对应函数的调用。它包含了在函数内部所有声明的变量与方法。并且能够建立一个新的 this 绑定。还支持捕获 super 方法调用所需要的所有参数。
function foo() {
const a = 20;
const b = 30;
return a + b;
}
// 那么 foo 对应的环境记录可以粗略表示为
fooRecord = {
a: 20,
b: 30,
[[outerEnv]]: GlobalEnvironmentRecord
}
在内部实现中,变量标识符与环境记录是绑定关系,我们这里只是使用 key-value 的形式来表达这种关系,并非真实呈现。每一个声明性环境记录通过 var、const、let、class、module、import、function 等方式与标识符进行绑定。
注意:在函数环境记录对象中,存在一个 this 属性。该属性在函数声明时并不确定,只有在函数被调用时,才能明确得知该值的具体指向。
模块环境记录
模块环境记录包含了所有顶层模块的声明。也包括显示导入的模块。他的内部属性 [[OuterEnv]] 指向全局环境记录。
对象环境记录
对象环境记录主要对应的是 with 语句创建的上下文环境。
全局环境记录
全局环境记录对应全局声明。它没有外部环境,内部属性 [[OuterEnv]] 的值为 null。它可能预装了标识符绑定,并且包括一个关联的全局对象,该对象的属性提供了某些全局环境的标识符绑定。在代码执行过程中,可以往该全局对象中添加新的属性并修改其值。
变量提升
环境记录用于收集各种声明的绑定。在内部实现中,我们可以关注环境记录用于初始化变量声明绑定的方法的描述,例如 **CreateMutableBinding,**该方法用于创建一个新的未被初始化的可变绑定,这里我们重点关注「未被初始化」几个字,也就意味着,此方法将会用于收集某种声明,并且不会立即赋值。可以用一个简单的例子验证。
console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 20
console.log(a) // undefined
var a = 20
Cannot access ‘a’ before initialization,未初始化之前不能访问变量 a。也就意味着,当我们使用 console.log 访问 a 时,a 已经完成了绑定,只不过还没有赋值。
当我们把上面例子中,变量声明的方式由 let 修改为 var 时,变量提升的影响会变得更为明显。在声明之前访问变量 a 的值,会发现该值为 undefined。这也是 let/const 与 var 的不同之处。
var a = undefined
console.log(a)
a = 20
<!-- 或者内部实现可以体现为 -->
// 初始化阶段创建绑定
envRec.CreateMutableBinding('a', true)
// 执行阶段设置值
console.log(a)
envRec.SetMutableBinding('a', 20, true)
使用 function 关键字声明的函数,在变量提升中的体现与 var/let/const 都不一样。function 声明的函数,在初始化时,就会直接赋值指向对应的函数体。
console.log(foo) // ƒ foo() {}
function foo() {}
使用 class 声明的对象,在变量提升中的体现与 let/const 一致。
console.log(A) // Uncaught ReferenceError: Cannot access 'A' before initialization
class A {}
新版本中的异常提示:在初始化之前不能访问变量,是在传递一种弱化变量提升概念的意图,变量提升是之前版本对于 undefined 的理解带来的历史遗留问题。
本质的核心是我们应该理解在执行上下文的创建阶段,环境记录对象会提前收集所有的声明绑定。而在代码执行阶段才会针对每个变量绑定进行赋值操作。
Local对象
对于一个函数而言,完整的作用域包含一个函数自身的 Local 对象。
仅仅只有处于栈顶的执行上下文,才会生成 Local 对象。并且 Local 对象的具体内容会在执行上下文的生命周期中不断变化。也就意味着,在执行上下文的创建阶段,只有函数参数、function 声明的变量、this 指向 能够明确具体的值,其他变量的初始值都为 undefined,然后在代码执行过程中逐步明确赋值。
需要注意的是,Local 对象与环境记录对象非常相似,但他们并非相同的对象。不过这样的差别仅仅只是体现在具体的实现上,从理解执行上下文运行机制的理论角度来说,我们可以认为他们是同一个对象。