簡單介紹的執行上下文和執行棧

什麼是執行上下文?

執行上下文是評估和執行 JavaScript 代碼的環境的抽象概念。Javascript 代碼都是在執行上下文中運行。javascript

JavaScript 的可執行代碼(executable code)的類型只有三種,全局代碼、函數代碼、eval代碼。前端

對應着,JavaScript 中有三種執行上下文類型。java

  • 全局執行上下文 — 默認的上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:建立一個全局的 window 對象(瀏覽器的狀況下),而且設置 this 的值等於這個全局對象。一個程序中只會有一個全局執行上下文。
  • 函數執行上下文 — 每當一個函數被調用時, 都會爲該函數建立一個新的上下文。每一個函數都有它本身的執行上下文,函數上下文能夠有任意多個。
  • Eval 函數執行上下文 — 執行在 eval 函數內部的代碼也會有它屬於本身的執行上下文

舉個栗子,當執行到一個函數的時候,就會進行準備工做,這裏的「準備工做」,就是準備"執行上下文(execution context)"。git

執行棧

執行棧,是一種擁有 LIFO(後進先出)數據結構的棧,被用來存儲代碼運行時建立的全部執行上下文。github

當 JavaScript 開始要解釋執行代碼的時候,它會建立一個全局的執行上下文而且壓入當前執行棧。每當引擎遇到一個函數調用,它會爲該函數建立一個新的執行上下文並壓入棧的頂部。面試

程序結束以前, 執行棧最底部永遠有個全局上下文瀏覽器

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

模擬js執行如下代碼:函數

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();
複製代碼

定義執行上下文棧:ECStack = [];post

  1. 向棧中壓入全局上下文:ECStack.push(globalContext);
  2. 執行fun1,建立fun1上下文,並壓入執行棧:ECStack.push(fun1Context);
  3. 執行fun2,建立fun2上下文,並壓入執行棧:ECStack.push(fun2Context);
  4. 執行fun3,建立fun3上下文,並壓入執行棧:ECStack.push(fun3Context);
  5. fun3執行完畢,彈出並銷燬fun3上下文:ECStack.pop();
  6. fun2執行完畢,彈出並銷燬fun2上下文:ECStack.pop();
  7. fun1執行完畢,彈出並銷燬fun1上下文:ECStack.pop();
  8. 全部代碼執行完畢,JavaScript 引擎從當前棧中移除全局執行上下文。

怎麼建立執行上下文?

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

在建立階段會發生三件事:

  1. This 綁定
  2. 建立詞法環境組件。
  3. 建立變量環境組件。

或者你也能夠簡單理解爲:

  1. 函數上下文環境參數的綁定(arguments)
  2. 函數表達式提高(hoist)
  3. 變量的聲明,並將var聲明的變量初始值設置爲 undefined (hoist)

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

ExecutionContext = {
  ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 複製代碼

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

詞法環境對象

詞法環境和變量環境組件始終爲 詞法環境對象。

變量環境也是一個詞法環境,它有着詞法環境的全部屬性。

在 ES6 中,詞法環境組件和變量環境的一個不一樣就是前者被用來和變量(letconst)綁定,然後者用來存儲函數聲明和 var 變量綁定。即:

  • let、const聲明的變量,外部環境引用保存在詞法環境組件中。
  • var和function聲明的變量和保存在環境變量組件中。

每一個詞法環境對象包含兩部分:

  • 環境記錄器
  • 外部環境的引用(可能爲空,好比全局詞法環境就沒有外部引用)

如下面代碼爲例:

let a = 1;
const b = 2;
var c = 3;
function test (d, e) {
  var f = 10;
  return f * d * e;
}
c = test(a, b);
複製代碼

解析階段的全局環境內的詞法環境和變量環境

GlobalLexicalEnvironment = {
  LexicalEnvironment: { // 詞法環境組件
    OuterReference: null, // 全局詞法環境中外部引用爲空
    EnviromentRecord: {
      Type: 'object',
      a: <uninitialized> , // let 和 const 變量綁定但未關聯值
      b: <uninitialized> 
    },
  },
  VariableEnvironment: { //變量環境組件
    EnviromentRecord: {
      type: 'object',
      test: <func>,
      c: undefined,  // var變量會被初始爲 undefined
    }
  }
}
複製代碼

解析test時的詞法環境和變量環境

注意:只有調用函數時,函數執行上下文才會被建立

// 此時 全局上下文已經執行,所以 a、b、c都已經與對應值關聯
GlobalLexicalEnvironment = {
  LexicalEnvironment: {
    OuterReference: null,
    EnviromentRecord: {
      Type: 'object',
      a: 1 ,
      b: 2 
    },
  },
  VariableEnvironment: {
    EnviromentRecord: {
      type: 'object',
      c: 3,,
      test: <func>
    }
  }
}

// test的詞法執行上下文開始構建,var變量綁定但未賦值,形參綁定
FunctionLexicalEnvironment = {
  LexicalEnvironment: {
    OuterReference:  <GlobalLexicalEnvironment>,
    EnviromentRecord: {
      Type: 'Declarative',
      arguments: {0: 1, 1: 2, length: 2}
    },
  },
  VariableEnvironment: {
    EnviromentRecord: {
      Type: 'Declarative',
      f: undefined,
    }
  }
}
複製代碼

插播一條變量提高的知識點:

在建立執行上下文時,js引擎會檢查當前做用域的全部變量聲明及函數聲明,在執行以前,var聲明的變量已經綁定初始undefined,而在let和const只綁定在了執行上下文中,但並未初始任何值,因此在聲明以前調用則會拋出引用錯誤(即TDZ暫時性死區),這也就是函數聲明與var聲明在執行上下文中的提高。

let/const也存在變量提高現象,詳情移至你可能不知道的變量提高

執行階段

在執行上下文的建立階段,完成了變量聲明,在代碼的執行階段,纔會完成對變量真正的賦值。

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

最後,看一個《JavaScript權威指南》中的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
複製代碼
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
複製代碼

兩段代碼執行的結果同樣,都是local scope,若不理解,請移步詞法做用域及做用域鏈講解

可是兩段代碼究竟有哪些不一樣呢?

模擬第一段代碼:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
複製代碼

模擬第二段代碼:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
複製代碼

ps: 這篇文章寫的很困難,蒐集資料的時候被各類詞語及講解弄的很懵,有些邏輯還有衝突,考慮了好久才決定只寫這些內容,將這篇文章只做爲對執行上下文的簡單描述而不是詳細講解,由於再寫多了,一些概念會使文章很難被閱讀和理解,等後續我有了深刻的理解再更新內容吧。若是有錯誤之處,歡迎在評論中指出~

相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)

若是你收穫了新知識,請給做者點個贊吧,讓更多的人看到它~

參考文章:

  1. ****JavaScript深刻之執行上下文棧****
  2. ****[譯] 理解 JavaScript 中的執行上下文和執行棧****
  3. ****也來談談JS的執行上下文與詞法環境****
相關文章
相關標籤/搜索