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

若是你是一名 JavaScript 開發者,或者想要成爲一名 JavaScript 開發者,那麼你必須知道 JavaScript 程序內部的執行機制。理解執行上下文和執行棧一樣有助於理解其餘的 JavaScript 概念如提高機制、做用域和閉包等。javascript

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

廢話很少說,讓咱們切入正題。java

什麼是執行上下文

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

執行上下文的類型

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

  • 全局執行上下文: 這是默認的、最基礎的執行上下文。不在任何函數中的代碼都位於全局執行上下文中。它作了兩件事:1. 建立一個全局對象,在瀏覽器中這個全局對象就是 window 對象。2. 將 this 指針指向這個全局對象。一個程序中只能存在一個全局執行上下文。
  • 函數執行上下文: 每次調用函數時,都會爲該函數建立一個新的執行上下文。每一個函數都擁有本身的執行上下文,可是隻有在函數被調用的時候纔會被建立。一個程序中能夠存在任意數量的函數執行上下文。每當一個新的執行上下文被建立,它都會按照特定的順序執行一系列步驟,具體過程將在本文後面討論。
  • Eval 函數執行上下文: 運行在 eval 函數中的代碼也得到了本身的執行上下文,但因爲 Javascript 開發人員不經常使用 eval 函數,因此在這裏再也不討論。

執行棧

執行棧,在其餘編程語言中也被叫作調用棧,具備 LIFO(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。編程

當 JavaScript 引擎首次讀取你的腳本時,它會建立一個全局執行上下文並將其推入當前的執行棧。每當發生一個函數調用,引擎都會爲該函數建立一個新的執行上下文並將其推到當前執行棧的頂端。數組

引擎會運行執行上下文在執行棧頂端的函數,當此函數運行完成後,其對應的執行上下文將會從執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文。瀏覽器

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

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() 函數執行完成後,它的執行上下文從當前執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文,即 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 開發人員,但對上述概念的理解將有助於你更輕鬆、更深刻地理解其餘概念,如提高、域和閉包等。

查看更多分享,請關注閱文集團前端團隊公衆號:

相關文章
相關標籤/搜索