【譯】JS的執行上下文和環境棧是什麼?

call_me_R發表於2019-03-10

這篇文章中,我將深入探討JavaScript中的一個最基本的部分,即執行上下文(或稱環境)。讀過本文後,你將更加清楚地瞭解到直譯器嘗試做什麼,為什麼在宣告某些函式/變數之前,可以使用它們以及它們的值是如何確定的。

執行上下文是什麼?

在執行JavaScript程式碼時,執行環境非常重要,並可以認為是以下其中之一:

  • 全域性程式碼 - 預設環境,你的程式碼第一時間在這裡執行。
  • 函式程式碼 - 當執行流進入函式體的時候。
  • Eval程式碼 - eval函式內部的文字。【eval不建議使用】

你可以在網上查到大量的關於scope(作用域)的資料,本文的目的就是要讓事情更加容易理解。我們把術語執行上下文視為當前程式碼的評估環境/範圍。現在,條件充足,我們看個包含全域性和函式/本地上下文評估程式碼的示例。

img1

這裡沒什麼特別的,我們有1個由紫色邊框表示的全域性上下文和由綠色、藍色和橙色邊框表示的3個不同的函式上下文。只有1個全域性上下文,我們可以從程式的任何其它上下文訪問。

你可以擁有任意數量的函式上下文,並且每個函式呼叫都會建立一個新的上下文,從而建立一個私有的作用域,無法從當前函式作用域外直接訪問函式內部宣告的任何內容。在上面的例子中,函式可以訪問在其當前上下文之外宣告的變數,但是外部上下文無法訪問(函式)其中宣告的變數/函式。為什麼會這樣?這段程式碼究竟是如何評估的?

環境棧

瀏覽器中的JavaScript直譯器是單執行緒實現的。這意味著在瀏覽器中一次只能發生一件事情,其它動作或事件在所謂的執行棧中排隊。下圖是單執行緒棧的抽象檢視:

img2

我們知道,當瀏覽器首次載入指令碼時,它預設進入全域性執行上下文。如果在全域性程式碼中呼叫一個函式,程式的順序流就進入被呼叫的函式,建立一個新的執行上下文並將該上下文推送到執行棧的頂部。

如果你在當前函式中呼叫另外一個函式,則會發生同樣的事情。程式碼的執行流程進入函式內部,該函式建立一個新的執行上下文,該上下文被推送到現有棧的頂部。瀏覽器將始終執行位於棧頂部的當前執行上下文,並且一旦函式完成當前執行上下文,它將從棧頂彈出,將控制權返回當前棧的棧頂上下文。下面的例子展示了遞迴函式和其程式的執行棧

(function foo(i) {
    if (i === 3) {
        return;
    }
    else {
        foo(++i);
    }
}(0));
複製程式碼

img3

上面程式碼只呼叫自身3次,將i的值遞增1。每次呼叫函式foo時,都會建立一個新的執行上下文。一旦上下文執行完畢,它就會彈出棧並且將控制權返回它下面的上下文,直到再次到達全域性上下文

關於執行棧有五個關鍵點:

  • 單執行緒
  • 同步執行
  • 1個全域性上下文
  • 無限的函式上下文
  • 每個函式呼叫都會建立一個新的執行上下文,甚至是呼叫自身

執行上下文的細節

所以,我們現在知道每次呼叫一個函式時,都會建立一個新的執行上下文。但是,在JavaScript的直譯器中,執行上下文的呼叫都有兩個階段:

  1. 建立階段【呼叫函式時,但是在執行裡面的程式碼之前】:
  • 建立作用域鏈
  • 建立變數,函式和引數
  • 確定this的值
  1. 啟用/程式碼執行階段:
  • 分配值,引用函式和解析/執行程式碼

可以將每個執行上下文在概念上標示為具有3個屬性的物件:

executionContextObj = {
    'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
    'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
    'this': {}
}
複製程式碼

活動/變數物件【AO/VO】

呼叫函式時,但在執行實際函式之前,會建立此executionContextObj。這被稱為階段1,即建立階段。這裡,直譯器通過掃描傳入的引數或引數的函式、本地函式宣告和區域性函式宣告來建立executionContextObj。此掃描的結果將稱為executionContextObj中的variableObject

以下是直譯器如何評估程式碼的偽概述:

  1. 找些程式碼來呼叫一個函式
  2. 在執行函式程式碼之前,建立執行上下文
  3. 進入建立階段
  • 初始化作用域鏈
  • 建立變數物件
    • 建立arguments物件,檢查引數的上下文,初始化名稱和值並建立引用的副本。
    • 掃描上下文以獲取函式宣告:
      • 對於找到的每個函式,在變數物件(或活動物件)中建立一個屬性,該屬性是確切的函式名稱,該函式具有指向記憶體中函式的引用指標。
      • 如果函式名已存在,則將覆蓋引用指標值。
    • 掃面上下文以獲取變數宣告:
      • 對於找到的每個變數宣告,在變數物件(或活動物件)中建立一個屬性,該屬性是變數名稱,並將值初始化為undefined。
      • 如果變數名稱已存在於變數物件(或活動物件)中,則不執行任何操作並繼續掃描(即跳過)。
    • 確定上下文中的this
  1. 啟用/程式碼執行階段:
  • 在上下文中執行/解釋功能程式碼,並在程式碼逐行執行時分配變數值。

看下下面的例子:

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);
複製程式碼

呼叫foo(22),建立階段如下:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}
複製程式碼

正如你所見,建立階段處理定義屬性的名稱,而不是為它們賦值,但正式引數/引數除外建立階段完成後,執行流程進入函式,啟用/程式碼執行階段在函式執行完畢後如下所示:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}
複製程式碼

“提升”一詞

你可以在網上找到很多定義JavaScript術語-提升的資源,解釋變數和函式宣告是否被提升到其功能範圍的頂部。但是,沒有人詳細解釋為什麼會發生這種情況,在掌握了關於直譯器如何建立活動物件的新知識點,就很容易理解為什麼了。看下下面的程式碼例子:

(function() {

    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };

    function foo() {
        return 'hello';
    }

}());​
複製程式碼

我們現在可以回答下面這些問題了:

  • 為什麼我們可以在宣告foo前訪問它?

    • 如果我們遵循建立階段,我們就知道在啟用/程式碼執行階段之前就已經建立了變數。因此,當函式開始執行時,已經在活動物件中定義了foo
  • Foo被宣告瞭兩次,為什麼foo顯示為函式而不是undefinedstring呢?

    • 即使foo被宣告瞭兩次,我們從建立階段中就知道到達變數之前在活動物件上已經建立了函式,並且如果活動物件上已經存在屬性名稱,我們就會繞過了宣告。
    • 因此,首先在活動物件上建立函式foo()的引用,並且當直譯器到達var foo時,我們已經看到名稱foo存在,因此程式碼什麼都不做並且繼續。
  • 為什麼bar是undefined

    • bar實際上是一個具有函式賦值的變數,我們知道變數是在建立階段建立的,但它們是使用undefined值初始化的。

總結

希望到現在,你已經很好地掌握了JavaScript直譯器是如何評估你的程式碼。理解執行上下文和環境棧可以讓你瞭解程式碼的評估和你預期不同值的原因。

你是認為了解直譯器的內部工作原理是多餘的還是必要的JavaScript知識點呢?知道執行上下文是否有助你編寫出更好的JavaScript?

筆記:有些人一直在詢問閉包,回撥,timeout等知識點,我將在下一篇文章中介紹,更多地關注與執行環境相關的作用域鏈

擴充套件閱讀

原文: http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/

文章首發:https://github.com/reng99/blogs/issues/11

更多內容:https://github.com/reng99/blogs

相關文章