【譯】理解 Javascript 執行上下文和執行棧

閱文前端團隊發表於2018-11-05

如果你是一名 JavaScript 開發者,或者想要成為一名 JavaScript 開發者,那麼你必須知道 JavaScript 程式內部的執行機制。理解執行上下文和執行棧同樣有助於理解其他的 JavaScript 概念如提升機制、作用域和閉包等。

正確理解執行上下文和執行棧的概念將有助於你成為一名更好的 JavaScript 開發人員。

廢話不多說,讓我們切入正題。

什麼是執行上下文

簡而言之,執行上下文就是當前 JavaScript 程式碼被解析和執行時所在環境的抽象概念, JavaScript 中執行任何的程式碼都是在執行上下文中執行。

執行上下文的型別

執行上下文總共有三種型別:

  • 全域性執行上下文: 這是預設的、最基礎的執行上下文。不在任何函式中的程式碼都位於全域性執行上下文中。它做了兩件事:1. 建立一個全域性物件,在瀏覽器中這個全域性物件就是 window 物件。2. 將 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 執行上下文和執行棧

當上述程式碼在瀏覽器中載入時,JavaScript 引擎會建立一個全域性執行上下文並且將它推入當前的執行棧。當呼叫 first() 函式時,JavaScript 引擎為該函式建立了一個新的執行上下文並將其推到當前執行棧的頂端。

當在 first() 函式中呼叫 second() 函式時,Javascript 引擎為該函式建立了一個新的執行上下文並將其推到當前執行棧的頂端。當 second() 函式執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文,即 first() 函式的執行上下文。

first() 函式執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到全域性執行上下文。一旦所有程式碼執行完畢,Javascript 引擎把全域性執行上下文從執行棧中移除。

執行上下文是如何被建立的

到目前為止,我們已經看到了 JavaScript 引擎如何管理執行上下文,現在就讓我們來理解 JavaScript 引擎是如何建立執行上下文的。

執行上下文分兩個階段建立:1)建立階段; 2)執行階段

建立階段

在任意的 JavaScript 程式碼被執行前,執行上下文處於建立階段。在建立階段中總共發生了三件事情:

  1. 確定 this 的值,也被稱為 This Binding
  2. LexicalEnvironment(詞法環境) 元件被建立。
  3. VariableEnvironment(變數環境) 元件被建立。

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

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 物件,因為沒有給出任何物件引用
複製程式碼

詞法環境(Lexical Environment)

官方 ES6 文件將詞法環境定義為:

詞法環境是一種規範型別,基於 ECMAScript 程式碼的詞法巢狀結構來定義識別符號與特定變數和函式的關聯關係。詞法環境由環境記錄(environment record)和可能為空引用(null)的外部詞法環境組成。

簡而言之,詞法環境是一個包含識別符號變數對映的結構。(這裡的識別符號表示變數/函式的名稱,變數是對實際物件【包括函式型別物件】或原始值的引用)

在詞法環境中,有兩個組成部分:(1)環境記錄(environment record) (2)對外部環境的引用

  1. 環境記錄是儲存變數和函式宣告的實際位置。
  2. 對外部環境的引用意味著它可以訪問其外部詞法環境。

詞法環境有兩種型別:

  • 全域性環境(在全域性執行上下文中)是一個沒有外部環境的詞法環境。全域性環境的外部環境引用為 null。它擁有一個全域性物件(window 物件)及其關聯的方法和屬性(例如陣列方法)以及任何使用者自定義的全域性變數,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 元件的區別在於前者用於儲存函式宣告和變數( 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

這是因為在建立階段,程式碼會被掃描並解析變數和函式宣告,其中函式宣告儲存在環境中,而變數會被設定為 undefined(在 var 的情況下)或保持未初始化(在 letconst 的情況下)。

這就是為什麼你可以在宣告之前訪問 var 定義的變數(儘管是 undefined ),但如果在宣告之前訪問 letconst 定義的變數就會提示引用錯誤的原因。

這就是我們所謂的變數提升。

執行階段

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

注: 在執行階段,如果 Javascript 引擎在原始碼中宣告的實際位置找不到 let 變數的值,那麼將為其分配 undefined 值。

總結

我們已經討論了 JavaScript 內部是如何執行的。雖然你沒有必要學習這些所有的概念從而成為一名出色的 JavaScript 開發人員,但對上述概念的理解將有助於你更輕鬆、更深入地理解其他概念,如提升、域和閉包等。

檢視更多分享,請關注閱文集團前端團隊公眾號:

【譯】理解 Javascript 執行上下文和執行棧

相關文章