[譯] 理解 JavaScript 中的執行上下文和執行棧

照片來自Unsplash 的作者Greg Rakozy
如果你是或者想成為一名 JavaScript 開發者,你必須知道 JavaScript 程式內部是如何執行的。理解執行上下文和執行棧對於理解其他 JavaScript 概念(如變數宣告提升,作用域和閉包)至關重要。
正確理解執行上下文和執行棧的概念將使您成為更出色的 JavaScript 開發者。
閒話少說,讓我們開始吧 :)
分享自Bit 的部落格 :heart:
使用 Bit 應用所提供的元件作為構建模組,你就是架構師。隨時隨地和你的團隊分享、發現和開發元件,快來嘗試鮮!
- ofollow,noindex"> Bit - 分享和創造程式碼元件 : Bit 能幫助你在不同專案和應用中分享、發現和使用程式碼元件來建立新功能和……
什麼是執行上下文?
簡而言之,執行上下文是評估和執行 JavaScript 程式碼的環境的抽象概念。每當 Javascript 程式碼在執行的時候,它都是在執行上下文中執行。
執行上下文的型別
JavaScript 中有三種執行上下文型別。
- 全域性執行上下文 — 這是預設或者說基礎的上下文,任何不在函式內部的程式碼都在全域性上下文中。它會執行兩件事:建立一個全域性的 window 物件(瀏覽器的情況下),並且設定
this
的值等於這個全域性物件。一個程式中只會有一個全域性執行上下文。 - 函式執行上下文 — 每當一個函式被呼叫時, 都會為該函式建立一個新的上下文。每個函式都有它自己的執行上下文,不過是在函式被呼叫時建立的。函式上下文可以有任意多個。每當一個新的執行上下文被建立,它會按定義的順序(將在後文討論)執行一系列步驟。
- Eval 函式執行上下文 — 執行在
eval
函式內部的程式碼也會有它屬於自己的執行上下文,但由於 JavaScript 開發者並不經常使用eval
,所以在這裡我不會討論它。
執行棧
執行棧,也就是在其它程式語言中所說的“呼叫棧”,是一種擁有 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 引擎從當前棧中移除全域性執行上下文。
怎麼建立執行上下文?
到現在,我們已經看過 JavaScript 怎樣管理執行上下文了,現在讓我們瞭解 JavaScript 引擎是怎樣建立執行上下文的。
建立執行上下文有兩個階段: 1) 建立階段 和 2) 執行階段 。
The Creation Phase
在 JavaScript 程式碼執行前,執行上下文將經歷建立階段。在建立階段會發生三件事:
- this 值的決定,即我們所熟知的 This 繫結 。
- 建立 詞法環境 元件。
- 建立 變數環境 元件。
所以執行上下文在概念上表示如下:
ExecutionContext = { ThisBinding = <this value>, 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 物件,因為 // 沒有指定引用物件 複製程式碼
詞法環境
官方的 ES6 文件把詞法環境定義為
詞法環境是一種規範型別,基於 ECMAScript 程式碼的詞法巢狀結構來定義 識別符號 和具體變數和函式的關聯。一個詞法環境由環境記錄器和一個可能的引用 外部 詞法環境的空值組成。
簡單來說 詞法環境 是一種持有 識別符號—變數對映 的結構。(這裡的 識別符號 指的是變數/函式的名字,而 變數 是對實際物件[包含函式型別物件]或原始資料的引用)。
現在,在詞法環境的 內部 有兩個元件:(1) 環境記錄器 和 (2) 一個 外部環境的引用 。
- 環境記錄器 是儲存變數和函式宣告的實際位置。
- 外部環境的引用 意味著它可以訪問其父級詞法環境(作用域)。
詞法環境有兩種型別:
- 全域性環境 (在全域性執行上下文中)是沒有外部環境引用的詞法環境。全域性環境的外部環境引用是 null 。它擁有內建的 Object/Array/等、在環境記錄器內的原型函式(關聯全域性物件,比如 window 物件)還有任何使用者定義的全域性變數,並且
this
的值指向全域性物件。 - 在 函式環境 中,函式內部使用者定義的變數儲存在 環境記錄器 中。並且引用的外部環境可能是全域性環境,或者任何包含此內部函式的外部函式。
環境記錄器也有兩種型別(如上!):
- 宣告式環境記錄器 儲存變數、函式和引數。
- 物件環境記錄器 用來定義出現在 全域性上下文 中的變數和函式的關係。
簡而言之,
- 在 全域性環境 中,環境記錄器是物件環境記錄器。
- 在 函式環境 中,環境記錄器是宣告式環境記錄器。
注意 —對於 函式環境 , 宣告式環境記錄器 還包含了一個傳遞給函式的 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); 複製程式碼
執行上下文看起來像這樣:
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 開發者並不需要學會全部這些概念,但是如果對上面概念能有不錯的理解將有助於你更輕鬆,更深入地理解其他概念,如變數宣告提升,作用域和閉包。
就是這樣,如果你發現這篇文章有用,請點選 :clap: 按鈕並在下面自由地評論!我很樂意和你討論 :smiley:。