JavaScript中的執行上下文

翻譯:瘋狂的技術宅
連接: http://davidshariff.com/blog/...

本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript


在這篇文章中,我將深刻探討JavaScript的最基本部分之一,即Execution Context(執行上下文)。 在本文結束時,你應該對解釋器瞭解得更清楚:爲何在聲明它們以前可使用某些函數或變量?以及它們的值是如何肯定的?前端

什麼是執行上下文?

JavaScript的執行環境很是重要,當JavaScript代碼在行時,會被預處理爲如下狀況之一:java

  • Global code - 首次執行代碼的默認環境。
  • Function code - 每當執行流程進入函數體時。
  • Eval code - 要在eval函數內執行的文本。

你能夠閱讀大量涉及做用域的在線資料,不過爲了使事情更容易理解,讓咱們將術語「執行上下文」視爲當前代碼的運行環境或做用域。接下來讓咱們看一個包含global和function / local上下文的代碼示例。瀏覽器

clipboard.png

這裏沒有什麼特別之處,咱們有一個由紫色邊框表示的全局上下文,和由綠色,藍色和橙色邊框表示的3個不一樣的函數上下文。 只能有1個全局上下文,能夠從程序中的任何其餘上下文訪問。微信

你能夠擁有任意數量的函數上下文,而且每一個函數調用都會建立一個新的上下文,從而建立一個私有做用域,其中沒法從當前函數做用域外直接訪問函數內部聲明的任何內容。 在上面的示例中,函數能夠訪問在其當前上下文以外聲明的變量,但外部上下文沒法訪問在其中聲明的變量或函數。 爲何會這樣呢? 這段代碼到底是如何處理的?ecmascript

Execution Context Stack(執行上下文堆棧)

瀏覽器中的JavaScript解釋器被實現爲單個線程。 實際上這意味着在瀏覽器中一次只能作一件事,其餘動做或事件在所謂的執行堆棧中排隊。 下圖是單線程堆棧的抽象視圖:ide

clipboard.png

咱們已經知道,當瀏覽器首次加載腳本時,它默認進入全局上下文執行。 若是在全局代碼中調用函數,程序的順序流進入被調用的函數,建立新的執行上下文並將其推送到執行堆棧的頂部。函數

若是在當前函數中調用另外一個函數,則會發生一樣的事情。 代碼的執行流程進入內部函數,該函數建立一個新的執行上下文,該上下文被推送到現有堆棧的頂部。 瀏覽器將始終執行位於堆棧頂部的當前執行上下文,而且一旦函數執行完當前執行上下文後,它將從棧頂部彈出,把控制權返回到當前棧中的下一個上下文。 下面的示例顯示了遞歸函數和程序的執行堆棧學習

(function foo(i) {
    if (i === 3) {
        return;
    }
    else {
        foo(++i);
    }
}(0));

clipboard.png

代碼簡單地調用自身3次,並將i的值遞增1。每次調用函數foo時,都會建立一個新的執行上下文。 一旦上下文完成執行,它就會彈出堆棧而且講控制返回到它下面的上下文,直到再次達到全局上下文this

關於執行堆棧execution stack有5個關鍵要點:

  • 單線程。
  • 同步執行。
  • 一個全局上下文。
  • 任意多個函數上下文。
  • 每一個函數調用都會建立一個新的執行上下文execution context,甚至是對自身的調用。

執行上下文的細節

因此咱們如今知道每次調用一個函數時,都會建立一個新的執行上下文。 可是,在JavaScript解釋器中,對執行上下文的每次調用都有兩個階段:

  1. 建立階段 [調用函數時,但在執行任何代碼以前]:

    • 建立做用域鏈
    • 建立變量,函數和參數。
    • 肯定「this」的值。
  2. 激活/代碼執行階段:

    • 分配值,引用函數和解釋/執行代碼。

能夠將每一個執行上下文在概念上表示爲具備3個屬性的對象:

executionContextObj = {
    'scopeChain': { /* variableObject + 全部父執行上下文的variableObject */ },
    'variableObject': { /* 函數實參/形參,內部變量和函數聲明 */ },
    'this': {}
}

激活對象/變量對象 [AO/VO]

在調用該函數,而且在實際執行函數以前,會建立這個executionContextObj。 這被稱爲第1階段,即創造階段。 這時解釋器經過掃描函數傳遞的實參或形參、本地函數聲明和局部變量聲明來建立executionContextObj。 此掃描的結果將成爲executionContextObj中的variableObject

如下是解釋器如何預處理代碼的僞代碼概述:

  1. 找一些代碼來調用一個函數。
  2. 在執行功能代碼以前,建立執行上下文
  3. 進入建立階段:

    • 初始化做用域鏈。
    • 建立variable object

      • 建立arguments object,檢查參數的上下文,初始化名稱和值並建立引用副本。
      • 掃描上下文以獲取函數聲明:

        • 對於找到的每一個函數,在variable object中建立一個屬性,該屬性是函數的確切名稱,該屬性存在指向內存中函數的引用指針。
        • 若是函數名已存在,則將覆蓋引用指針值。
      • 掃描上下文以獲取變量聲明:

        • 對於找到的每一個變量聲明,在variable object中建立一個屬性做爲變量名稱,並將該值初始化爲undefined
        • 若是變量名稱已存在於variable object中,則不執行任何操做並繼續掃描。
    • 肯定上下文中「this」的值。
  4. 激活/執行階段:

    • 在上下文中運行/解釋函數代碼,並在代碼逐行執行時分配變量值。

咱們來看一個例子:

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: { ... }
}

關於hoisting

你能夠找到許多使用JavaScript定義術語hoisting的在線資源,解釋變量和函數聲明被hoisting到其函數範圍的頂部。 可是沒有人可以詳細解釋爲何會發生這種狀況,掌握了關於解釋器如何建立激活對象的新知識,很容易理解爲何。 請看下面的代碼示例:

(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顯示爲function而不是undefinedstring

    • 即便foo被聲明兩次,咱們經過建立階段知道函數在變量以前就被建立在激活對象上了,並且若是激活對象上已經存在了屬性名稱,咱們只是繞過了聲明這一步驟。
    • 所以,首先在激活對象上建立對函數foo()的引用,而且當解釋器到達var foo時,咱們已經看到屬性名稱foo存在,所以代碼不執行任何操做並繼續處理。
  • 爲何bar未定義?

    • bar其實是一個具備函數賦值的變量,咱們知道變量是在建立階段被建立的,但它們是使用undefined值初始化的。

總結

但願到這裏你已經可以很好地掌握了JavaScript解釋器如何預處理你的代碼。 理解執行上下文和堆棧可讓你瞭解背後的緣由:爲何代碼預處理後的值和你預期的不同。

你認爲學習解釋器的內部工做原理是畫蛇添足仍是很是必要的呢? 瞭解執行上下文階段是否可以幫你你寫出更好的JavaScript呢?

進一步閱讀


本文首發微信公衆號:jingchengyideng

歡迎掃描二維碼關注公衆號,天天推送我翻譯的技術文章歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

相關文章
相關標籤/搜索