Closed lxfriday closed 4 years ago
一起看看 JavaScript 程序内部是如何执行的。
本文翻译自 https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0,作者 Sukhjinder Arora,有部分删改。
如果你想成为一个合格的 JavaScript 开发者,你必须知道它的内部是如何执行的。掌握 JavaScript 执行上下文和执行栈对理解变量提升、作用域和闭包非常重要。
理解执行上下文和执行栈将使你成为一个更加优秀的 JavaScript 开发者。
执行上下文是一个 JavaScript 代码运行的环境。任何 JavaScript 代码执行的时候都是处于一个执行上下文中。
JavaScript 中一共有三种执行上下文。
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 执行阶段。
执行上下文在创建阶段就会被创建。创建阶段做下面两件事:
所以从概念上说,执行上下文可以用下面的方式表示:
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>,
}
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>
}
每个词法环境由三个部分组成:
this
Environment Record 是在词法环境中存储变量和函数的地方。
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>
}
}
变量环境也是一个词法环境,它和词法环境长得一样。区别在于,在 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 引擎会创建一个全局执行上下文来执行全局的代码。所以在创建阶段(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
,全局词法环境将会更新,然后所有代码执行完毕,程序结束。
你可能注意到了 let
和 const
声明的变量在创建阶段(creation phase) 和它的值没有任何关联,但是 var
声明的变量被赋予了 undefined
。
这是因为在创建阶段 JavaScript 引擎会扫描到变量和函数声明。用 var
声明的变量被初始化为 undefined
,用 let
const
声明的变量将不会被初始化。后者将会形成暂时性死区,提前使用它们将会报错。
这就是变量提升。
注意,在执行阶段,如果引擎发现 let
声明的变量并没有被赋值,引擎将会把它赋值为 undefined
。
感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术。关注公众号可以拉你进讨论群,有任何问题都会回复。
形成文章
理解 JavaScript 中的执行上下文
ref #2