一塊兒看看 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,帶你解讀前端技術。。