JavaScript 中的執行上下文和執行棧
1. 什麼是執行上下文?
簡而言之,執行上下文是評估和執行 JavaScript 代碼的環境的抽象概念。每當 Javascript 代碼在運行的時候,它都是在執行上下文中運行。
執行上下文的類型
JavaScript 中有三種執行上下文類型。
- 全局執行上下文(GlobalExectionContext) : 這是默認或者說基礎的上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:創建一個全局的 window 對象(瀏覽器的情況下),並且設置
this
的值等於這個全局對象。一個程序中只會有一個全局執行上下文。 - 函數執行上下文(FunctionExectionContext) : 每當一個函數被調用時, 都會爲該函數創建一個新的上下文。每個函數都有它自己的執行上下文,不過是在函數被調用時創建的。函數上下文可以有任意多個。每當一個新的執行上下文被創建,它會按定義的順序(將在後文討論)執行一系列步驟。
- Eval 函數執行上下文 :執行在
eval
函數內部的代碼也會有它屬於自己的執行上下文,但由於 JavaScript 開發者並不經常使用eval
,所以在這裏我不會討論它。
2.執行棧
執行棧,也就是在其它編程語言中所說的“調用棧”,是一種擁有 LIFO(後進先出)數據結構的棧,被用來存儲代碼運行時創建的所有執行上下文。
當 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()
函數調用時,JavaScript 引擎爲該函數創建一個新的執行上下文並把它壓入當前執行棧的頂部。 -
當從
first()
函數內部調用second()
函數時,JavaScript 引擎爲second()
函數創建了一個新的執行上下文並把它壓入當前執行棧的頂部。當second()
函數執行完畢,它的執行上下文會從當前棧彈出,並且控制流程到達下一個執行上下文,即first()
函數的執行上下文。 -
當
first()
執行完畢,它的執行上下文從棧彈出,控制流程到達全局執行上下文。一旦所有代碼執行完畢,JavaScript 引擎從當前棧中移除全局執行上下文。
3. 怎麼創建執行上下文?
創建執行上下文有兩個階段:1) 創建階段 和 2) 執行階段。
創建階段:
在 JavaScript 代碼執行前,執行上下文將經歷創建階段。在創建階段會發生三件事:
-
this 值的決定,即 This 綁定(ThisBinding)。
-
創建詞法環境(LexicalEnvironment)組件。
-
創建變量環境(VariableEnvironment)組件。
所以執行上下文在概念上表示如下:
ExecutionContext = {
ThisBinding = <this value>,//This 綁定
LexicalEnvironment = { ... },//詞法環境
VariableEnvironment = { ... },//變量環境
}
This 綁定:
-
在全局執行上下文中,
this
的值指向全局對象。(在瀏覽器中,this
引用 Window 對象)。 -
在函數執行上下文中,
this
的值取決於該函數是如何被調用的。如果它被一個引用對象調用,那麼this
會被設置成那個對象,否則this
的值被設置爲全局對象或者undefined
(在嚴格模式下)。 -
例如:
let foo = { baz: function() { console.log(this); } } foo.baz(); // 'this' 引用 'foo', 因爲 'baz' 被 // 對象 'foo' 調用 let bar = foo.baz; bar(); // 'this' 指向全局 window 對象,因爲 // 沒有指定引用對象
詞法環境:
-
詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符和具體變量和函數的關聯。一個詞法環境由環境記錄器和一個可能的引用外部詞法環境的空值組成。
簡單來說詞法環境是一種持有標識符—變量映射的結構。(這裏的標識符指的是變量/函數的名字,而變量是對實際對象[包含函數類型對象]或原始數據的引用)。
-
詞法環境有兩種類型:
-
全局環境:(在全局執行上下文中)是沒有外部環境引用的詞法環境。其外部環境引用是 null。它擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,
this
的值指向這個全局對象。 -
在函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了
arguments
對象。對外部環境的引用可以是全局環境,也可以是包含內部函數的外部函數環境。
-
-
詞法環境的內部有兩個組件:
- 環境記錄器 (EnvironmentRecord):存儲變量和函數聲明的實際位置。
- 外部環境的引用(outer) :可以訪問其父級(其外部)詞法環境(作用域)。
-
環境記錄器也有兩種類型:
- 聲明式環境記錄器存儲變量、函數和參數。
- 對象環境記錄器用來定義出現在全局上下文中的變量和函數的關係。
簡而言之:
- 在全局環境中,環境記錄器是對象環境記錄器。
- 在函數環境中,環境記錄器是聲明式環境記錄器。
對於函數環境,聲明式環境記錄器還包含了一個傳遞給函數的
arguments
對象(此對象存儲索引和參數的映射)和傳遞給函數的參數的 length。 -
抽象地講,詞法環境在僞代碼中看起來像這樣:
//以下是全局執行上下文中的詞法環境 GlobalExectionContext = { LexicalEnvironment: {//詞法環境 EnvironmentRecord: {//聲明式環境記錄器 Type: "Object", // 在這裏綁定標識符 } outer: <null>//外部環境的引用 } } //以下函數執行上下文中的詞法環境 FunctionExectionContext = { LexicalEnvironment: {//詞法環境 EnvironmentRecord: {//對象環境記錄器 Type: "Declarative", // 在這裏綁定標識符 } outer: <Global or outer function environment reference>//外部環境的引用 } }
變量環境:
-
它同樣是一個詞法環境,其環境記錄器持有變量聲明語句在執行上下文中創建的綁定關係。
-
變量環境也是一個詞法環境,所以它有着上面定義的詞法環境的所有屬性。
-
在 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);
執行上下文看起來像這樣:
//以下是全局執行上下文,包括This 綁定,詞法環境,變量環境
GlobalExectionContext = {
ThisBinding: <Global Object>, //This 綁定
LexicalEnvironment: {//詞法環境,包括對象環境記錄器,外部環境的引用
EnvironmentRecord: {//對象環境記錄器
Type: "Object",
// 在這裏綁定標識符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>//外部環境的引用
},
VariableEnvironment: {//變量環境,包括對象環境記錄器,外部環境的引用
EnvironmentRecord: {//對象環境記錄器
Type: "Object",
// 在這裏綁定標識符
c: undefined,
}
outer: <null>//外部環境的引用
}
}
//以下函數執行上下文,包括This 綁定,詞法環境,變量環境
FunctionExectionContext = {
ThisBinding: <Global Object>,//This 綁定
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
。
參考整理於:link