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

這篇文章中,我將深刻探討JavaScript中的一個最基本的部分,即執行上下文(或稱環境)。讀過本文後,你將更加清楚地瞭解到解釋器嘗試作什麼,爲何在聲明某些函數/變量以前,可使用它們以及它們的值是如何肯定的。javascript

執行上下文是什麼?

在運行JavaScript代碼時,執行環境很是重要,並能夠認爲是如下其中之一:java

  • 全局代碼 - 默認環境,你的代碼第一時間在這裏執行。
  • 函數代碼 - 當執行流進入函數體的時候。
  • Eval代碼 - eval函數內部的文本。【eval不建議使用】

你能夠在網上查到大量的關於scope(做用域)的資料,本文的目的就是要讓事情更加容易理解。咱們把術語執行上下文視爲當前代碼的評估環境/範圍。如今,條件充足,咱們看個包含全局和函數/本地上下文評估代碼的示例。git

img1

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

你能夠擁有任意數量的函數上下文,而且每一個函數調用都會建立一個新的上下文,從而建立一個私有的做用域,沒法從當前函數做用域外直接訪問函數內部聲明的任何內容。在上面的例子中,函數能夠訪問在其當前上下文以外聲明的變量,可是外部上下文沒法訪問(函數)其中聲明的變量/函數。爲何會這樣?這段代碼到底是如何評估的?瀏覽器

環境棧

瀏覽器中的JavaScript解釋器是單線程實現的。這意味着在瀏覽器中一次只能發生一件事情,其它動做或事件在所謂的執行棧中排隊。下圖是單線程棧的抽象視圖:閉包

img2

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

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

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

img3

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

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

  • 單線程
  • 同步執行
  • 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

相關文章
相關標籤/搜索