JavaScript执行上下文
执行上下文
当控制权转移到ECMAScript可执行代码时,控制权正在进入执行上下文。活动执行上下文在逻辑上形成一个堆栈。此逻辑堆栈顶部的执行上下文是运行中的执行上下文。
执行上下文(execution context) 指的就是一段代码运行的环境。
调用栈由执行上下文组成,所以在JavaScript中执行上下文就是所谓的栈帧,调用栈的顶部是正在运行的代码的执行上下文,底部是全局的执行上下文。
变量对象
可执行代码类型:
- 全局代码
eval
代码- 函数代码
根据 ES3 文档,执行上下文中的内容包括:
- variable object
- scope chain
- this
全局对象
也就是 globalThis
,在浏览器端就是 window
,node 就是 global
,全局对象包含了 ECMAScript 的内置对象,和宿主环境定义的附加属性。
全局对象在进入任何执行上下文之前被创建,全局对象也是全局的 VO,即所谓的 GO。
变量对象
当进入一个函数的执行上下文时,会创建一个变量对象并和该执行上下文相关联,并使用一个名为 arguments
的属性初始化该对象。
变量对象会被作为用于变量初始化的可变对象。
变量对象也就是所谓的 VO,还有一个 AO,就是执行上下文栈顶端的执行上下文中的 VO,也就是正在执行的函数所对应的 VO。
变量初始化
所有的执行上下文都与一个可变对象相关联,在源代码文本中声明的变量和函数都被作为属性添加给这个可变对象。对于函数代码,参数被作为属性添加给这个可变对象。
进入执行上下文时,按照下面的顺序把属性绑定到可变对象:
对于函数形参:其名称为属性名,值为传递的实参。没传递就为
undefined
。如果有两个或多个形参重名(会为同一属性),最后一个使用此名称的参数给对应的属性提供值。若最后一个未给出,对应的属性的值即为undefined
。对于函数声明:按源代码文本的顺序创建该可变对象的属性,名称为该函数名,值为该函数对象。如果这个可变对象已经拥有了与此同名的属性,替换这个属性的值和特征。
对于变量声明:也就是
var
声明的变量,创建该可变对象的属性,值为undefined
,如果这个可变对象已经拥有了与被声明变量的同名的属性,该属性的值和特征不变。
该过程也就是有些地方所说的预编译/预解析,也就是变量提升的原因。
参数对象
当控制进入函数代码的执行上下文时,创建并初始化一个参数对象 arguments
,初始化过程:
- 参数对象内部属性
[[Prototype]]
的值是原始的 Object 原型对象,即为一个 plain-object - 创建名为
callee
的属性,初始值为被执行的函数对象 - 创建名为
length
的属性,初始值为提供给调用者的实参值的个数 - 对每个小于
length
属性的非负整数arg
,初始值为对应的提供给调用者的实际参数。对于arg
小于函数对象的形参个数的情况,该属性与对应活动对象的属性同值。这就意味着改变这个属性也会改变对应活动对象的属性,反之亦然。
作用域链
每一个执行上下文都与一个作用域链相关联。作用域链是一个对象组成的链表,求值 标识符 的时候会搜索它。
当控制进入执行上下文时,就根据代码类型创建一个作用域链,并用初始化对象填充。
具体的过程就是,每个函数在创建(定义)时,有一个属性 [[Scope]]
,该属性保存了当时执行上下文中的作用域链。
接着在创建执行上下文的时候,将该函数的 VO 放在链表头,作用域链存放的就是 VO 的集合(由 VO 组成的一个链表)。
进入执行上下文
也可以说是创建执行上下文的步骤,因为具有三种不同类型的代码,所以创建方式也分为三种。
也可以称为有三种执行上下文:全局执行上下文、函数执行上下文、eval执行上下文。
进入不同代码,创建上下文的步骤:
全局
- 被创建并初始化的作用域链只包含全局代码
- 进行变量初始化时,把全局对象作为可变对象
this
值为全局对象
函数
- 被创建并初始化的作用域链包含一个活动对象,该对象之后是函数对象的
[[Scope]]
属性存储的作用域链中的对象 - 进行变量初始化时,把该活动对象作为可变对象
this
值由调用者提供。若调用者提供的this
值不是一个对象,则this
值为全局对象
eval
当控制进入求值代码的执行上下文时,把前一个活动的执行上下文引用为调用上下文,用它决定作用域链、可变对象和 this
值。若调用上下文不存在,就把它当作全局对象,进行作用域链和变量的初始化及 this
值的决定。
- 作用域链与调用上下文包含相同的对象,顺序也一样;使用
with
声明或catch
语句给调用上下文的作用域链添加的对象也包括在内。 - 进行变量初始化时,使用调用上下文的可变对象
this
值与调用上下文的this
值相同
词法环境
从 ES5开始,就不存在VO这些东西了,而是由词法环境代替。
ES6+ 执行上下文中包含了:
- LexicalEnvironment
- VariableEnvironment
- 其他的许多东西
Lexical Environment
词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由环境记录和对外部词法环境的可能为空的引用组成。通常,词法环境与ECMAScript代码的某些特定句法结构(例如,函数声明,BlockStatement或TryStatement的Catch子句)相关联,并且每次运行此类代码时都会创建一个新的词法环境。
简单来说,词法环境的组成:
- 环境记录
Environment Record
:存放变量和函数声明的地方 - 外层引用
outer
:父词法环境的引用,可能为null
词法环境分类
全局环境 global environment
全局环境的外部环境引用为 null
。全局环境的 Environment Record 可能预填充了标识符绑定,在执行ECMAScript代码时,可以将其他属性添加到全局对象,并且可以修改初始属性。
模块环境 module environment
模块环境其中包含模块顶级声明的绑定,它还包含模块显式导入的绑定。模块环境的外部环境是全局环境。
函数环境 function environment
函数环境对应于ECMAScript函数对象的调用,一个函数环境可以建立一个新的 this
绑定,函数环境还支持 super
调用所必需的状态。
环境记录
Environment Record分为:
- Declarative Environment Record
- Object Environment Record
- Function Environment Record
- Global Environment Record
- Module Environment Record
出于规范目的,环境记录值是Record规范类型的值并且可以认为它存在于简单的面向对象的层次结构中,其中Environment Record是具有三个具体子类的抽象类,分别是declarative Environment Record, object Environment Record, 和 global Environment Record。
Function Environment Records 和 module Environment Records是declarative Environment Record子类。
像 var
、const
、let
、class
、function
、import
这些声明都是属于声明性环境记录,而且还包含了一个传递给函数的 arguments
对象。
环境记录的类型不重要,只需要知道我们所声明的变量、函数以及内置对象都被保存在 Environment Record 中。
变量环境
Variable Environment 本质上也是一个 Lexical Environment,只不过通过 var
声明的变量保存在这里,而通过 let
等方式声明的变量存储在Lexical Environment。
1 | let a = 20; |
执行上下文伪代码:
1 | GlobalExectionContext = { |
总结
很明显,首先词法环境的定义就明确说出 Block + 特定的语法会创建一个新的词法环境,也就表名带来了块级作用域。
词法环境中的 record 和 outer 就相当于以前的 VO 和作用域链,特殊的 VariableEnvironment 也很好的将 var
与 let
、const
声明区分。