原文章资料:Understanding Execution Context and Execution Stak in Javascript
什么是 Excution Context
简单的说,执行上下文是 Javascript 代码计算和被执行环境的抽象概念。Javascript 代码在哪里执行,哪里就是执行上下文。
Excution Context 的种类
- 全局执行上下文 —— 最基础的执行上下文,一个运行的 Javascript 程序只存在一个全局执行上下文。写在函数体之外的代码都处在全局执行上下文。他做了两件事情:创建了一个全局对象(浏览器中是window对象,node环境中是global),并把全局对象赋值给 this。
- 函数执行上下文 —— 每当函数被调用,函数内部就会生成一个新的执行上下文。每个函数都有自己的执行上下文,但是只有当函数被调用的时候,才会被创建。
-
eval
函数执行上下文 ——eval
函数中的代码也有自己的执行上下文,但是eval
并不常用。
Excution Stack
执行栈,又称“调用栈”,是一个先进后出的结构(LIFO, Last in First Out),用来存放代码执行过程中所有的执行上下文。
Javascript 引擎在执行代码时,创建并把全局执行上下文压入 Excution Stack;每当执行到函数调用的时候,就会创建新的函数执行上下文,然后从栈顶端推入栈中。
Javascript 引擎从顶端开始执行函数,当函数被执行完成后,它的执行上下文就会被推出栈,然后再去执行栈中的下一个上下文。
🌰 简单的例子:
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');
执行上下文是如何创建的
创建上下文分为两步:
- 创建阶段
- 执行阶段
创建阶段:
LexicalEnvironment:词法环境
VariableEnvironment:变量环境
- 创建词法环境组件
- 创建变量环境组件
Lexical Environment
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
词法环境是用来定义 基于ECMAScript代码词法嵌套结构的具体变量和函数的关联 的具体类型。词法环境由环境记录器和一个只想外部词法环境的 引用(outer) 组成。
每一个 词法环境 由以下结构组成:
- Environment Record
- Reference to the outer environment
- This binding
Environment Record
存储词法环境中 定义的变量和函数。
- 声明式环境记录器存储变量、函数和参数。
- 对象环境记录器用来定义出现在全局上下文中的变量和函数,并且存储了全局对象包含的方法和属性。
对于 函数内部的词法环境,环境记录器还会记录一个 arguments
对象(存储参数的索引和值的键值对)和参数的长度。这个结构看上去如何下:
function foo(a, b) { var c = a + b }
// argument object
Arguments: {0: 2, 1: 3, length: 2}
Reference to the outer environment
外部环境的引用意味着它可以访问其父级词法环境(作用域)
this 的绑定
在全局执行上下文中,this
的值指向全局对象。(在浏览器中,this
引用 Window 对象)。
在函数执行上下文中,this
的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this
会被设置成那个对象,否则 this
的值被设置为全局对象或者 undefined
(在严格模式下)。例如:
const person = {
name: 'peter',
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}
person.calcAge();
// 'this' refers to 'person', because 'calcAge' was called with //'person' object reference
const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given
抽象来说,词法环境的结构类似下面代码表示:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
}
outer: <null>,
this: <global object>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
},
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",
// Identifier bindings go here
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
在变量分配在执行阶段完成,全局执行上下文如下:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: 20,
b: 30,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
}
当执行函数 multiply(20, 30)
,就会创建一个新的函数执行上下文来执行函数代码:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
}
之后执行上下文来到 执行阶段,这意味着已完成对函数内部变量的分配。
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: 20
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
}
函数执行之后,返回值存到了变量 c
中,随后全局词法环境被更新了,全局的代码运行完成,程序结束。
上面我们看到 let
和 const
声明的变量并没有被初始化,但是 var
定义的变量被设置为 undefined
(这对应了 let
和 const
没有变量提示这一特性)。
这是因为在创建阶段,Javascript 引擎读取了所有的代码,找到了变量和函数的定义,其中函数声明被完整的存在了环境中,而变量被初始化称 undefined
(如果是 var
声明的变量)或者保持未初始化(如果是 let
和 const
声明的变量)。
这就是为什么 var
声明的变量可以在声明之前被访问到,而 let
和 const
声明的变量不允许被访问(reference error)。
这就是常说的 变量提升(varaible hoisting) 。