一块儿看看 JavaScript 程序内部是如何执行的。javascript
本文翻译自 blog.bitsrc.io/understandi…,做者 Sukhjinder Arora,有部分删改。前端
若是你想成为一个合格的 JavaScript 开发者,你必须知道它的内部是如何执行的。掌握 JavaScript 执行上下文和执行栈对理解变量提高、做用域和闭包很是重要。java
理解执行上下文和执行栈将使你成为一个更加优秀的 JavaScript 开发者。编程
执行上下文是一个 JavaScript 代码运行的环境。任何 JavaScript 代码执行的时候都是处于一个执行上下文中。windows
JavaScript 中一共有三种执行上下文。数组
window
)而且会把 this
设置为全局对象 windows
。在一个程序中只会有一个全局执行上下文。eval
函数执行上下文 -- 在 eval
函数中执行的代码也会有本身的自行上下文,但因为 eval
已经不经常使用了,因此不作讨论。执行栈(执行上下文栈),在其余编程语言中也叫调用栈,是一个后进先出的结构。它用来存储代码执行过程当中建立的全部执行上下文。浏览器
当 JavaScript 引擎执行你的代码时,它会建立一个全局执行上下文而且将它推入当前的执行栈。当执行碰到函数调用的时候,它会为这个函数建立执行上下文并把这个执行上下文推入执行栈顶部。闭包
引擎执行处于栈顶的上下文对应的函数。当函数执行完毕,它的上下文就会从栈顶弹出,引擎接着继续执行新处于顶部的上下文对应的函数。编程语言
看看下面的例子:ide
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,带你解读前端技术。。