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


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


譯者序

最近在研究 JavaScript 基礎性的東西,但是看到對於 執行上下文 的解釋我發現有兩種,一種是執行上下文包含:scope(作用域)、variable object(變量對象)、this value(this 值),另外一個種是包含:lexical environment(詞法環境)、variable environment(變量環境)、this value(this 值)。

後面我查閱了不少博客以及 ES3 和 ES5 的規範才瞭解到,第一種是 ES3 的規範,經典書籍《JavaScript高級程序設計》第三版就是這樣解釋的,也是網上廣為流傳的一種,另一種是 ES5 的規範。

然後我接著又去翻了 ES2018 中的,發現又有變化了,已經增加了更多的內容了,考慮到這部分內容頗為複雜,準備後面再進行總結分享,查資料的時候看到這篇講執行上下文(ES5 )的還不錯,所以就翻譯出來先分享給大家。

以後看到變量對象、活動對象知道是 ES3 裡面的內容,而如果是詞法環境、變量環境這種詞就是 ES5 以後的內容。

什麼是執行上下文?

簡而言之,執行上下文是計算和執行 JavaScript 代碼的環境的抽象概念。每當 Javascript 代碼在運行的時候,它都是在執行上下文中運行。

執行上下文的類型

JavaScript 中有三種執行上下文類型。

  • 全局執行上下文 — 這是默認或者說基礎的上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:創建一個全局的 window 對象(瀏覽器的情況下),並且設置 this 的值等於這個全局對象。一個程序中只會有一個全局執行上下文。
  • 函數執行上下文 — 每當一個函數被調用時, 都會為該函數創建一個新的上下文。每個函數都有它自己的執行上下文,不過是在函數被調用時創建的。函數上下文可以有任意多個。每當一個新的執行上下文被創建,它會按定義的順序(將在後文討論)執行一系列步驟。
  • Eval 函數執行上下文 — 執行在 eval 函數內部的代碼也會有它屬於自己的執行上下文,但由於 JavaScript 開發者並不經常使用 eval ,所以在這裡我不會討論它。

執行棧

執行棧,也就是在其它編程語言中所說的“調用棧”,是一種擁有 LIFO(後進先出)的數據結構,被用來存儲代碼運行時創建的所有執行上下文。

當 JavaScript 引擎第一次遇到你的腳本時,它會創建一個全局的執行上下文並且壓入當前執行棧。每當引擎遇到一個函數調用,它會為該函數創建一個新的執行上下文並壓入棧的頂部。

引擎會執行處於棧頂的執行上下文的函數。當該函數執行結束時,執行上下文從棧中彈出,控制流程到達當前棧中的下一個上下文。

讓我們通過下面的代碼示例來理解:

<code>let a = 'Hello World!';

functionfirst() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}

functionsecond() {
console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');/<code>
理解 JavaScript 中的執行上下文和執行棧

上述代碼的執行上下文棧。

當上述代碼在瀏覽器加載時,JavaScript 引擎創建了一個全局執行上下文並把它壓入當前執行棧。當遇到 first() 函數調用時,JavaScript 引擎為該函數創建一個新的執行上下文並把它壓入當前執行棧的頂部。

當從 first() 函數內部調用 second() 函數時,JavaScript 引擎為 second() 函數創建了一個新的執行上下文並把它壓入當前執行棧的頂部。當 second() 函數執行完畢,它的執行上下文會從當前棧彈出,並且控制流程到達下一個執行上下文,即 first() 函數的執行上下文。

當 first() 執行完畢,它的執行上下文從棧彈出,控制流程到達全局執行上下文。一旦所有代碼執行完畢,JavaScript 引擎從當前棧中移除全局執行上下文。

怎麼創建執行上下文?

到現在,我們已經看過 JavaScript 怎樣管理執行上下文了,現在讓我們瞭解 JavaScript 引擎是怎樣創建執行上下文的。

創建執行上下文有兩個階段: 1) 創建階段2) 執行階段

創建階段

在 JavaScript 代碼執行前,執行上下文將經歷創建階段。在創建階段會發生三件事:

  1. this 值的決定,即我們所熟知的 this 綁定
  2. 創建 詞法環境 組件。
  3. 創建 變量環境 組件。

所以執行上下文在概念上表示如下:

<code>ExecutionContext = {
ThisBinding = <this>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}/<this>/<code>

this 綁定:**

在全局執行上下文中, this 的值指向全局對象。(在瀏覽器中, this 引用 Window 對象)。

在函數執行上下文中, this 的值取決於該函數是如何被調用的。如果它被一個引用對象調用,那麼 this 會被設置成那個對象,否則 this 的值被設置為全局對象或者 undefined (在嚴格模式下)。例如:

<code>let foo = {
baz: function() {
console.log(this);
}
}

foo.baz(); // 'this' 引用 'foo', 因為 'baz' 被
// 對象 'foo' 調用

let bar = foo.baz;

bar(); // 'this' 指向全局 window 對象,因為
// 沒有指定引用對象/<code>

詞法環境

官方的 ES6 文檔把詞法環境定義為

詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義 標識符 和具體變量和函數的關聯。一個詞法環境由環境記錄器和一個可能的引用 outer 詞法環境的空值組成。

簡單來說 詞法環境 是一種持有 標識符—變量映射 的結構。(這裡的 標識符 指的是變量/函數的名字,而 變量

是對實際對象[包含函數類型對象]或原始數據的引用)。

現在,在詞法環境的 內部 有兩個組件:(1) 環境記錄器 和 (2) 一個 外部環境的引用

  1. 環境記錄器 是存儲變量和函數聲明的實際位置。
  2. 外部環境的引用 意味著它可以訪問其父級詞法環境(作用域)。

譯者注:外部環境已經跟 ES3 規定的作用域的作用類似

詞法環境有兩種類型:

  • 全局環境 (在全局執行上下文中)是沒有外部環境引用的詞法環境。全局環境的外部環境引用是 null 。它擁有內建的 Object/Array/等、在環境記錄器內的原型函數(關聯全局對象,比如 window 對象)還有任何用戶定義的全局變量,並且 this 的值指向全局對象。
  • 函數環境 中,函數內部用戶定義的變量存儲在 環境記錄器 中。並且引用的外部環境可能是全局環境,或者任何包含此內部函數的外部函數。

環境記錄器也有兩種類型(如上!):

  1. 聲明式環境記錄器 存儲變量、函數和參數。
  2. 對象環境記錄器 用來定義出現在 全局上下文 中的變量和函數的關係。

簡而言之,

  • 全局環境 中,環境記錄器是對象環境記錄器。
  • 函數環境 中,環境記錄器是聲明式環境記錄器。

注意 —對於 函數環境聲明式環境記錄器 還包含了一個傳遞給函數的 arguments 對象(此對象存儲索引和參數的映射)和傳遞給函數的參數的 length

抽象地講,詞法環境在偽代碼中看起來像這樣:

<code>GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這裡綁定標識符
}
outer: <null>
}
}

FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這裡綁定標識符
}
outer: <global>
}
}/<global>/<null>/<code>

變量環境:

它同樣是一個詞法環境,其環境記錄器持有 變量聲明語句 在執行上下文中創建的綁定關係。

如上所述,變量環境也是一個詞法環境,所以它有著上面定義的詞法環境的所有屬性。

在 ES6 中, 詞法環境 組件和 變量環境 的一個不同就是前者被用來存儲函數聲明和變量( let 和 const )綁定,而後者只用來存儲 var 變量綁定。

我們看點樣例代碼來理解上面的概念:

<code>let a = 20;
const b = 30;
var c;

function multiply(e, f) {
var g = 20;
return e * f * g;
}

c = multiply(20, 30);/<code>

執行上下文看起來像這樣:

<code>GlobalExectionContext = {

ThisBinding: <global>,

LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這裡綁定標識符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},

VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這裡綁定標識符
c: undefined,
}
outer: <null>
}
}

FunctionExectionContext = {
ThisBinding: <global>,

LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這裡綁定標識符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <globallexicalenvironment>
},

VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這裡綁定標識符
g: undefined
},
outer: <globallexicalenvironment>
}
}/<globallexicalenvironment>/<globallexicalenvironment>/<global>/<null>/<null>/<global>/<code>

注意— 只有遇到調用函數 multiply 時,函數執行上下文才會被創建。

可能你已經注意到 let 和 const 定義的變量並沒有關聯任何值,但 var 定義的變量被設成了 undefined 。

這是因為在創建階段時,引擎檢查代碼找出變量和函數聲明,雖然函數聲明完全存儲在環境中,但是變量最初設置為 undefined ( var 情況下),或者未初始化( let 和 const 情況下)。

這就是為什麼你可以在聲明之前訪問 var 定義的變量(雖然是 undefined ),但是在聲明之前訪問 let 和 const 的變量會得到一個引用錯誤。

執行階段

這是整篇文章中最簡單的部分。在此階段,完成對所有這些變量的分配,最後執行代碼。

注意— 在執行階段,如果 JavaScript 引擎不能在源碼中聲明的實際位置找到 let 變量的值,它會被賦值為 undefined 。

結論

我們已經討論過 JavaScript 程序內部是如何執行的。雖然要成為一名卓越的 JavaScript 開發者並不需要學會全部這些概念,但是如果對上面概念能有不錯的理解將有助於你更輕鬆,更深入地理解其他概念,如變量聲明提升,作用域和閉包。


分享到:


相關文章: