logo头像

云影sky

前端面试必会 | 一文读懂 JavaScript 中的执行上下文和执行上下文栈


一起看看 JavaScript 程序内部是如何执行的。



本文翻译自 https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0,作者 Sukhjinder Arora,有部分删改。



如果你想成为一个合格的 JavaScript 开发者,你必须知道它的内部是如何执行的。掌握 JavaScript 执行上下文和执行栈对理解变量提升、作用域和闭包非常重要。


理解执行上下文和执行栈将使你成为一个更加优秀的 JavaScript 开发者。


执行上下文是什么?


执行上下文是一个 JavaScript 代码运行的环境。任何 JavaScript 代码执行的时候都是处于一个执行上下文中。


执行上下文的类型


JavaScript 中一共有三种执行上下文。



  • 全局执行上下文(Global) – 它是默认的基本执行上下文。代码要么在全局执行上下文要么在函数执行上下文。它有两个特征:它会创建一个全局对象(在浏览器中就是 window)并且会把 this 设置为全局对象 windows。在一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 – 当函数执行的时候,一个新的函数执行上下文就会创建。每个函数都有自己的执行上下文,当函数执行的时候上下文会被创建。函数执行上下文可以创建任意多个,每个执行上下文被创建的时候会经历若干步骤,接下来将会讨论。
  • eval 函数执行上下文 – 在 eval 函数中执行的代码也会有自己的自行上下文,但由于 eval 已经不常用了,所以不做讨论。

执行栈


执行栈(执行上下文栈),在其他编程语言中也叫调用栈,是一个后进先出的结构。它用来存储代码执行过程中创建的所有执行上下文。


当 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’);

上面代码的执行上下文栈
上面代码的执行上下文栈

上面代码在浏览器中执行时,JavaScript 引擎会先创建一个全局执行上下文并把它推出执行栈中。碰到 first() 执行时,引擎给这个函数创建一个新的执行上下文,然后把它推入执行栈顶部。


second()first() 函数内部执行时,引擎会给 second 创建上下文并把它推入执行栈顶,当 second 函数执行完毕,它的执行上下文就会从执行栈顶弹出,指针会指向它下面的上下文,也就是 first 函数的上下文。


first 函数执行完毕,它的执行栈也会从栈顶弹出,指针就指向了全局执行上下文。当所有的代码执行完毕,引擎会把全局执行上下文也从执行栈中移出。


执行上下文是如何创建的


从上面的过程,我们已经了解了 JavaScript 引擎是如何管理执行上下文的,接下来我们看看引擎是如何创建执行上下文的。


执行上下文会经历两个阶段:1 创建阶段;2 执行阶段。


创建阶段


执行上下文在创建阶段就会被创建。创建阶段做下面两件事:



  1. 创建词法环境(LexicalEnvironment)
  2. 创建变量环境(VariableEnvironment)

所以从概念上说,执行上下文可以用下面的方式表示:


ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>,
}

词法环境(Lexical Environment)


ES6 官方文档是这样定义词法环境的



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.



简单来说,词法环境是一种表示标识符和变量的映射关系的环境。在词法环境中,标识符指向变量或者函数,变量是指对象(包括函数对象和数组对象)或者原始值。


举个例子,看看下面的代码


var a = 20;
var b = 40;
function foo() {
console.log(‘bar’);
}

上面代码的词法环境如下


lexicalEnvironment = {
a: 20,
b: 40,
foo: <ref. to foo function>
}

每个词法环境由三个部分组成:



  1. 词法环境内部的环境记录(Environment Record);
  2. 一个指向外层词法环境的可空引用(Reference to the outer environment);
  3. this

环境记录(Environment Record)

Environment Record 是在词法环境中存储变量和函数的地方。


Environment Record 有下面两种:



  • Declarative environment record – As its name suggests stores variable and function declarations. The lexical environment for function code contains a declarative environment record.
  • Object environment record – The lexical environment for global code contains a objective environment record. Apart from variable and function declarations, the object environment record also stores a global binding object (window object in browsers). So for each of binding object’s property (in case of browsers, it contains properties and methods provided by browser to the window object), a new entry is created in the record.

上面是原文,简单解释下:



  • Declarative environment record – 用来放变量或者函数声明,函数中的词法环境都是这种;
  • Object environment record – 指向全局对象 window(在浏览器中),全局词法环境是这种;

注意:对于函数,环境记录也包括一个 arguments 对象。arguments 是一个类数组对象,它包含索引和参数值的映射。看看下面的例子:


function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},

outer 是什么

outer 表示一个作用域指向的外层词法环境。在查找变量时,如果在当前的词法环境里面没有找到变量,那就通过 outer 找到外层的词法环境,然后再在外层的词法环境里面查找变量,如果还没有找到,则会继续往外层找,一直找到全局作用域。


this 怎么确定

在全局执行上下文中,this 指向全局对象 window(在浏览器中)。


在函数执行上下文中,this 取决于函数是如何被调用的。这是我们经常弄混的一点。如果是通过对象调用的函数,那 this 指向这个对象。否则 this 将会指向全局对象(在浏览器中是 window)或者 undefined(严格模式下) 。 看下面的例子:


const person = {
name: ‘peter’,
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}
person.calcAge();
// ‘this’ 指向 ‘person’, 因为 ‘calcAge’ 是通过 person 对象调用的。
const calculateAge = person.calcAge;
calculateAge();
// ‘this’ 指向全局对象,因为函数不是通过对象引用的方式调用的。

词法作用域用伪代码表示是这样的:


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 中,词法环境用来存储函数声明和 letconst 声明的变量,变量环境仅仅用来存储 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 引擎会创建一个全局执行上下文来执行全局的代码。所以在创建阶段(creation phase)全局执行上下文是像这样的:


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>
}
}

在执行阶段(execution phase),会进行变量赋值。全局执行上下文将会变成下面这样:


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) 时,一个新的函数执行上下文会创建。在创建阶段(creation phase)函数执行上下文会像下面这样:


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>
}
}

在执行阶段(execution phase)会进行变量赋值。赋值之后的函数执行上下文如下:


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,全局词法环境将会更新,然后所有代码执行完毕,程序结束。


你可能注意到了 letconst 声明的变量在创建阶段(creation phase) 和它的值没有任何关联,但是 var 声明的变量被赋予了 undefined


这是因为在创建阶段 JavaScript 引擎会扫描到变量和函数声明。用 var 声明的变量被初始化为 undefined,用 let const 声明的变量将不会被初始化。后者将会形成暂时性死区,提前使用它们将会报错。


暂时性死区
暂时性死区

这就是变量提升。


注意,在执行阶段,如果引擎发现 let 声明的变量并没有被赋值,引擎将会把它赋值为 undefined



最后


感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术。关注公众号可以拉你进讨论群,有任何问题都会回复。


公众号
公众号