执行上下文、执行栈、作用域链、闭包,这其实是一整套相关的东西,之前转载的文章也有讲到这些。下面两篇文章会更加详细地解释这些概念。
1.执行上下文
1.1 定义
执行上下文(execution context)是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。
1.2 类型
- 全局执行上下文
只有一个。它创建了一个全局对象(浏览器中是window对象),并将this指向该对象。 - 函数执行上下文
无数个。每次调用函数时,都会为该函数创建一个新的执行上下文。 - eval 函数执行上下文
运行在 eval 函数中的代码也获得了自己的执行上下文,eval 函数不常用,所以这里不讨论
2.执行栈
执行栈(execution stack),也即调用栈(call stack),具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
当 JavaScript 引擎首次读取脚本时,它会创建一个全局执行上下文并将其 push 到当前的执行栈。每当调用函数的时候,都会为该函数创建一个新的执行上下文并将其 push 到栈顶;在函数执行完毕后,对应的执行上下文将会从栈顶 pop 出,上下文控制权将移到当前执行栈的下一个执行上下文。
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
3.执行上下文的创建
执行上下文分两个阶段创建:
- 创建阶段(The Creation Phase)
- 执行阶段(The Execution Phase)
3.1 创建阶段
- 词法环境组件被创建
- 变量环境组件被创建
用伪代码表示就是:
ExecutionContext = {
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
3.1.1 词法环境
词法环境(Lexical environment)是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)
词法环境有三个组成部分:
- 环境记录:存储变量和函数声明的实际位置
- 对外部环境的引用:可以访问其外部词法环境
- this绑定:确定this的指向
词法环境有两种类型:
- 全局环境:全局执行上下文的词法环境。
- 函数环境:函数执行上下文的词法环境。
3.1.1.1 环境记录:
根据词法环境的两种类型,环境记录(Environment record)同样也有两种类型:
对象环境记录(Object environment record):
全局环境的环境记录类型。存储全局变量和函数声明、全局对象(window 对象)和关联的属性/方法。声明性环境记录(Declarative environment record):
函数环境的环境记录类型。存储局部变量和函数声明、arguments
对象。arguments
对象包含了索引与参数之间的映射,以及传给函数的参数的个数。function foo(a, b) { var c = a + b; } foo(2, 3); // argument object(类数组的对象) Arguments: {0: 2, 1: 3, length: 2},
3.1.1.2 外部环境引用:
外部环境引用(Reference to the outer environment)表明当前词法环境能够访问外部词法环境。这意味着如果JavaScript引擎未在当前词法环境找到变量,它将向外部词法环境寻找(这有点类似原型链中的属性查找)
全局环境没有外部环境,其外部环境引用为 null。
函数环境有外部环境,其外部环境引用可以是全局环境,也可以是包含内部函数的外部函数环境。
3.1.1.3 this 绑定:
全局执行上下文中,this 绑定(this binding)到全局对象(对于浏览器,该对象为 window);函数执行上下文中,this 绑定到谁将取决于函数的调用位置(或者说调用方法)。
我会在另一篇文章总结 this 的绑定机制,所以这里不再展开。
讲完了词法环境的三个组成部分,最后再配合伪代码理解一下:
// 全局执行上下文
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
}
outer: <null>,
this: <global object>
}
}
// 函数执行上下文
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
}
outer: <Global or outer function environment reference>,
this: <depends on how function is called>
}
}
3.1.2 变量环境
变量环境(Variable environment)同样也是词法环境,因此它具有上面定义的词法环境的所有特征。这两者的区别主要在于:
在 ES6 中,词法环境用于存储函数声明和 let/const
变量绑定,而变量环境仅用于存储 var
变量绑定。
3.2 执行阶段
在执行阶段,完成对所有变量的分配,最后执行代码。
3.3 举例说明
通过一个例子来了解执行上下文的整个创建和执行过程。
以下面的代码为例
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
在开始读取代码后,JavaScript 引擎创建全局执行上下文并压栈,全局执行上下文的创建阶段的伪代码如下:
GlobalExectionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
之后进入全局执行上下文的执行阶段,开始进行变量分配/赋值,伪代码如下:
GlobalExectionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
a: 20,
b: 30,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
随着执行阶段的进行,我们遇到了multiply(20, 30)
,这是一个函数调用语句,所以此时创建了该函数对应的函数执行上下文并压栈,函数执行上下文的创建阶段的伪代码如下:
FunctionExectionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
之后进入函数执行上下文的执行阶段,开始进行函数内的变量的分配/赋值,伪代码如下:
FunctionExectionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: 20
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
函数执行完毕,函数执行上下文出栈,此时的执行上下文是全局执行上下文。由于函数的返回值被赋给变量 c,此时全局执行上下文对应的全局词法环境得到更新,伪代码如下:
GlobalExectionContext = {
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
a: 20,
b: 30,
multiply: <func>
}
outer: <null>,
ThisBinding: <Global Object>
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: 12000,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
全局执行上下文的执行阶段结束,程序结束。
4. 再看变量提升
在上面全局执行上下文创建阶段的伪代码中我们可以看到,let
和const
定义的变量没有任何与之关联的值,处于一个未初始化的状态,但var
定义的变量设置为undefined
。
这是因为在创建阶段,JavaScript 引擎会扫描一遍代码并解析所有的变量和函数声明,其中函数声明被存储在环境记录中,而变量的情况则比较特殊:var
声明的变量将被设置为undefined
,let
和const
声明的变量将保持未初始化。
因此,我们可以在声明之前就访问var
定义的变量,尽管是undefined
;但如果在声明之前访问let
和const
定义的变量则会提示引用错误,因为在执行阶段之前其始终是未初始化的,处于 TDZ 暂时性死区。
这就是我们所谓的变量提升。
注: 在执行阶段,如果Javascript引擎在源代码中声明的实际位置找不到 let
变量的值,那么将为其分配undefined
值。
5. 补充:关于 this binding
本文参考自:
如果你发现译文和原文在 this binding 的说法上存在出入,例如:
在原文中:
The execution context is created during the creation phase. Following things happen during the creation phase:
1.LexicalEnvironment component is created.
2.VariableEnvironment component is created.
Each Lexical Environment has three components:
1.Environment Record
2.Reference to the outer environment,
3.This binding
在译文中:
在任何 JavaScript 代码执行之前,执行环境经历了创建阶段,创建阶段包含以下三个事:
1.this 的值确定,也被称为 This Binding.
2.Lexical Environment 被创建。
3.Variable Environment 被创建。
在词法环境中,有两种组件:
(1) environment record
(2) reference to the outer environment.
下面这篇文章的评论区进行了解释:
总而言之是由于 ECAMAScript 的标准变更导致的。原文最初是基于 ES5 编写的,this 绑定的确是执行上下文创建阶段的一环,但是在 ES2018 的规范中,this 绑定被并入词法环境的环境记录,所以原作者后来进行了更改。