若是你是一名 JavaScript 開發者,或者想要成爲一名 JavaScript 開發者,那麼你必須知道 JavaScript 程序內部的執行機制。理解執行上下文和執行棧一樣有助於理解其餘的 JavaScript 概念如提高機制、做用域和閉包等。編程
正確理解執行上下文和執行棧的概念將有助於你成爲一名更好的 JavaScript 開發人員。數組
廢話很少說,讓咱們切入正題。瀏覽器
簡而言之,執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行。閉包
執行上下文總共有三種類型:編程語言
this
指針指向這個全局對象。一個程序中只能存在一個全局執行上下文。eval
函數中的代碼也得到了本身的執行上下文,但因爲 Javascript 開發人員不經常使用 eval 函數,因此在這裏再也不討論。執行棧,在其餘編程語言中也被叫作調用棧,具備 LIFO(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。ide
當 JavaScript 引擎首次讀取你的腳本時,它會建立一個全局執行上下文並將其推入當前的執行棧。每當發生一個函數調用,引擎都會爲該函數建立一個新的執行上下文並將其推到當前執行棧的頂端。函數
引擎會運行執行上下文在執行棧頂端的函數,當此函數運行完成後,其對應的執行上下文將會從執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文。學習
讓咱們經過下面的代碼示例來理解這一點:this
let a = 'Hello World!';spa
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()
函數時,JavaScript 引擎爲該函數建立了一個新的執行上下文並將其推到當前執行棧的頂端。
當在 first()
函數中調用 second()
函數時,Javascript 引擎爲該函數建立了一個新的執行上下文並將其推到當前執行棧的頂端。當 second()
函數執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文,即 first()
函數的執行上下文。
當 first()
函數執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到全局執行上下文。一旦全部代碼執行完畢,Javascript 引擎把全局執行上下文從執行棧中移除。
到目前爲止,咱們已經看到了 JavaScript 引擎如何管理執行上下文,如今就讓咱們來理解 JavaScript 引擎是如何建立執行上下文的。
執行上下文分兩個階段建立: 1)建立階段; 2)執行階段
在任意的 JavaScript 代碼被執行前,執行上下文處於建立階段。在建立階段中總共發生了三件事情:
所以,執行上下文能夠在概念上表示以下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
This Binding:
在全局執行上下文中, this
的值指向全局對象,在瀏覽器中, this
的值指向 window 對象。
在函數執行上下文中, this
的值取決於函數的調用方式。若是它被一個對象引用調用,那麼 this
的值被設置爲該對象,不然 this
的值被設置爲全局對象或 undefined
(嚴格模式下)。例如:
let person = {
name: 'peter',
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}
person.calcAge();
// 'this' 指向 'person', 由於 'calcAge' 是被 'person' 對象引用調用的。
let calculateAge = person.calcAge;
calculateAge();
// 'this' 指向全局 window 對象,由於沒有給出任何對象引用
官方 ES6 文檔將詞法環境定義爲:
詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關係。詞法環境由環境記錄(environment record)和可能爲空引用(null)的外部詞法環境組成。
簡而言之,詞法環境是一個包含 標識符變量映射 的結構。(這裏的 標識符 表示變量/函數的名稱, 變量 是對實際對象【包括函數類型對象】或原始值的引用)
在詞法環境中,有兩個組成部分:(1) 環境記錄(environment record) (2) 對外部環境的引用
詞法環境有兩種類型:
this
的值指向這個全局對象。注意:對於 函數環境 而言, 環境記錄 還包含了一個 arguments
對象,該對象包含了索引和傳遞給函數的參數之間的映射以及傳遞給函數的參數的 長度(數量) 。例如,下面函數的 arguments
對象以下所示:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// arguments 對象
Arguments: {0: 2, 1: 3, length: 2},
環境記錄一樣有兩種類型(以下所示):
抽象地說,詞法環境在僞代碼中看起來像這樣:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這裏
outer: <null>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這裏
outer: <Global or outer function environment reference>
}
}
它也是一個詞法環境,其 EnvironmentRecord
包含了由 VariableStatements 在此執行上下文建立的綁定。
如上所述,變量環境也是一個詞法環境,所以它具備上面定義的詞法環境的全部屬性。
在 ES6 中, LexicalEnvironment 組件和 VariableEnvironment 組件的區別在於前者用於存儲函數聲明和變量( 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);
執行上下文以下所示:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這裏
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這裏
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這裏
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這裏
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
注意:只有在遇到函數 multiply
的調用時纔會建立函數執行上下文。
你可能已經注意到了 let
和 const
定義的變量沒有任何與之關聯的值,但 var
定義的變量設置爲 undefined
。
這是由於在建立階段,代碼會被掃描並解析變量和函數聲明,其中函數聲明存儲在環境中,而變量會被設置爲 undefined
(在 var
的狀況下)或保持未初始化(在 let
和 const
的狀況下)。
這就是爲何你能夠在聲明以前訪問 var
定義的變量(儘管是 undefined
),但若是在聲明以前訪問 let
和 const
定義的變量就會提示引用錯誤的緣由。
這就是咱們所謂的變量提高。
這是整篇文章中最簡單的部分。在此階段,完成對全部變量的分配,最後執行代碼。
注:在執行階段,若是 Javascript 引擎在源代碼中聲明的實際位置找不到 let
變量的值,那麼將爲其分配 undefined
值。
咱們已經討論了 JavaScript 內部是如何執行的。雖然你沒有必要學習這些全部的概念從而成爲一名出色的 JavaScript 開發人員,但對上述概念的理解將有助於你更輕鬆、更深刻地理解其餘概念,如提高、域和閉包等。