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

照片來自 Unsplash 的做者 Greg Rakozyjavascript

若是你是或者想成爲一名 JavaScript 開發者,你必須知道 JavaScript 程序內部是如何執行的。理解執行上下文和執行棧對於理解其餘 JavaScript 概念(如變量聲明提高,做用域和閉包)相當重要。前端

正確理解執行上下文和執行棧的概念將使您成爲更出色的 JavaScript 開發者。vue

閒話少說,讓咱們開始吧 :)java


分享自 Bit 的博客 ❤️

使用 Bit 應用所提供的組件做爲構建模塊,你就是架構師。隨時隨地和你的團隊分享、發現和開發組件,快來嘗試鮮!react


什麼是執行上下文?

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

執行上下文的類型

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

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

執行棧

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

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

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

讓咱們經過下面的代碼示例來理解:

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 代碼執行前,執行上下文將經歷建立階段。在建立階段會發生三件事:

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

因此執行上下文在概念上表示以下:

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) 一個外部環境的引用

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

詞法環境有兩種類型:

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

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

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

簡而言之,

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

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

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

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

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在這裏綁定標識符
    }
    outer: <Global or outer function environment reference>
  }
}
複製代碼

變量環境:

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

如上所述,變量環境也是一個詞法環境,因此它有着上面定義的詞法環境的全部屬性。

在 ES6 中,詞法環境組件和變量環境的一個不一樣就是前者被用來存儲函數聲明和變量(letconst)綁定,然後者只用來存儲 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 時,函數執行上下文才會被建立。

可能你已經注意到 letconst 定義的變量並無關聯任何值,但 var 定義的變量被設成了 undefined

這是由於在建立階段時,引擎檢查代碼找出變量和函數聲明,雖然函數聲明徹底存儲在環境中,可是變量最初設置爲 undefinedvar 狀況下),或者未初始化(letconst 狀況下)。

這就是爲何你能夠在聲明以前訪問 var 定義的變量(雖然是 undefined),可是在聲明以前訪問 letconst 的變量會獲得一個引用錯誤。

這就是咱們說的變量聲明提高。

執行階段

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

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

結論

咱們已經討論過 JavaScript 程序內部是如何執行的。雖然要成爲一名卓越的 JavaScript 開發者並不須要學會所有這些概念,可是若是對上面概念能有不錯的理解將有助於你更輕鬆,更深刻地理解其餘概念,如變量聲明提高,做用域和閉包。

就是這樣,若是你發現這篇文章有用,請點擊 👏 按鈕並在下面自由地評論!我很樂意和你討論 😃。


分享自 Bit 的博客

Bit 使得在項目和應用中分享小型組件和模塊變得很是簡單,使您和您的團隊能夠更快地構建代碼。隨時隨地和你的團隊分享、發現和開發組件,快來嚐鮮!


瞭解更多

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索