执行上下文、执行栈、作用域链、闭包,这其实是一整套相关的东西,之前转载的文章也有讲到这些。下面两篇文章会更加详细地解释这些概念。
执行上下文(execution context)是当前 JavaScript 代码被解析和执行时所在环境的抽象概念,
执行栈(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
执行上下文分两个阶段创建:1)创建阶段(The Creation Phase); 2)执行阶段(The Execution Phase)
用伪代码表示就是:
ExecutionContext = {
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
词法环境(Lexical environment)是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)
词法环境有三个组成部分:
词法环境有两种类型:
根据词法环境的两种类型,环境记录(Environment record)同样也有两种类型:
arguments
对象。arguments
对象包含了索引与参数之间的映射,以及传给函数的参数的个数。function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},
外部环境引用(Reference to the outer environment)表明当前词法环境能够访问外部词法环境。这意味着如果JavaScript引擎未在当前词法环境找到变量,它将向外部词法环境寻找(这有点类似原型链中的属性查找)
全局环境没有外部环境,其外部环境引用为 null。
函数环境有外部环境,其外部环境引用可以是全局环境,也可以是包含内部函数的外部函数环境。
全局执行上下文中,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>
}
}
变量环境(Variable environment)同样也是词法环境,因此它具有上面定义的词法环境的所有特征。这两者的区别主要在于: 在 ES6 中,词法环境用于存储函数声明和变量(let和const)绑定,而变量环境仅用于存储变量(var)绑定。
在执行阶段,完成对所有变量的分配,最后执行代码。
通过一个例子来了解执行上下文的整个创建和执行过程。 以下面的代码为例
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>
}
}
全局执行上下文的执行阶段结束,程序结束。
补充:
在全局执行上下文创建阶段的伪代码中我们可以看到,let
和const
定义的变量没有任何与之关联的值,但var
定义的变量设置为undefined
。
这是因为在创建阶段,JavaScript引擎会扫描一遍代码并解析所有的变量和函数声明,其中函数声明被存储在环境记录中,而变量的情况则比较特殊:var
声明的变量将被设置为undefined
,let
和const
声明的变量将保持未初始化。
因此,我们可以在声明之前就访问var
定义的变量(尽管是undefined
),但如果在声明之前访问let
和const
定义的变量则会提示引用错误(因为在执行阶段之前其始终是未初始化的)。
这就是我们所谓的变量提升。
注: 在执行阶段,如果Javascript引擎在源代码中声明的实际位置找不到 let
变量的值,那么将为其分配undefined
值。
注意: 如果你发现译文和原文的说法存在出入,例如: 在原文中:
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绑定的确是执行上下文创建阶段的一环,但是在ES2015 ES2018 的规范中,this绑定被并入词法环境的环境记录,所以原作者后来进行了更改,只是各种翻译和转载没有改过来就是了。关于具体内容,可以参考:
ES5规范
ES6规范
上图的文章