詳解js執行環境——聲明提高的本質

​(function() {
    console.log(typeof foo); // 這裏會打印出什麼?
    console.log(typeof bar); // 這裏會打印出什麼?
    var foo = 'hello',
        bar = function() {
            return 'vian';
        };
    function foo() {
        return 'hello';
    }
})();​
複製代碼

咱們在接觸JavaScript這門語言時,會常常遇到這種問題,通過後續的學習,咱們可能知道了這種現象在JavaScript中叫聲明提高(hoisting),可是咱們可能只知道聲明提高的現象,卻不清楚形成這種現象的本質,而這個本質倒是JavaScript最爲重要的知識之一。解答上述代碼中的問題以前,咱們先看一下JavaScript中的執行環境。es6

執行環境(執行上下文)

執行環境(execution context)定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中。雖然咱們編寫的代碼沒法訪問這個對象,但解析器在處理數據時會在後臺使用它。瀏覽器

在JavaScript中可執行代碼能夠分爲三類:bash

  • 全局代碼 - JavaScript 代碼開始運行的默認環境,即全局的、不在任何函數裏面的代碼
  • 函數代碼 - 函數體內的代碼
  • eval代碼 - eval(...)函數中動態執行的代碼(咱們這裏不討論)

全局執行環境是最外圍的一個執行環境。在Web瀏覽器中,全局執行環境是window對象。當代碼載入瀏覽器時,全局執行環境被建立,直到網頁或瀏覽器被關閉,全局執行環境才被銷燬。 在一個Javascript程序中,必須且只能存在一個全局執行環境,和任意個的非全局執行環境,程序每調用一個函數,都會建立新的執行環境,以下圖所示。函數

圖一.png
該JavaScript程序中含有一個全局執行環境、三個函數執行環境。爲以正確的順序執行代碼,JavaScript中用堆棧的形式來處理執行環境——執行環境棧(Execution Context Stack)。

執行環境棧

瀏覽器中JavaScript是單線程的,這意味着運行在瀏覽器中的JavaScript代碼,同一時間只有一件事件、動做發生。除了當前正在運行的代碼,其餘代碼都在一個隊列中排隊,這個隊列就是執行環境棧。 爲了更好的說明執行環境棧,咱們結合一個例子來講明:學習

(function test(i) {
    if (i === 1) {
        return;
    }
    test(++i);
})(0);
複製代碼

下面咱們用圖來表示上述代碼執行時,執行環境棧中的變化過程: ui

ecs1.png

  1. 當JavaScript代碼執行時,第一個建立的老是全局執行環境,所以全局執行環境也老是在執行環境棧的最底部。 this

    ecs2.png

  2. 代碼執行時,調用了函數test(0),此時建立新的執行環境test EC,並壓入執行棧。 spa

    ecs3.png

  3. 在執行第一次執行函數test(i)時(i=0),執行到函數體中的代碼test(++i),程序再一次調用函數test(i),建立新的執行環境test EC1,並壓入執行棧。 線程

    ecs4.png

  4. 函數test(i = 1)中的代碼執行完畢,該執行環境出棧銷燬,程序回到上一層執行環境中繼續執行。 code

    esc5.png

5.函數test(i = 0)中的代碼執行完畢,該執行環境出棧銷燬,程序回到全局執行環境中繼續執行,全局環境的代碼即便執行完畢,也不會銷燬,直至網頁或瀏覽器被關閉了纔出棧銷燬。

上面就是程序執行過程當中,執行環境棧的變成過程。關於執行環境棧,有如下幾點要特別注意:

  • 單線程
  • 同步執行
  • 只有一個全局執行環境
  • 能夠有無數個的非全局執行環境
  • 每一次函數調用都會建立一個新的執行環境,即便是調用自身

詳解執行環境

從上文咱們已經知道了,每當一個函數被調用時,一個新的執行環境就會被建立。實際上,執行環境能夠分爲兩個階段,建立階段和執行階段。JavaScript聲明提高的祕密也在其中,咱們繼續往下看。

  • 建立階段 (函數被調用,同時在執行函數內的代碼前) 在這個階段會發生如下的事: 建立變量對象(VO,Variable Object) 創建做用域鏈(Scope Chain) 肯定this的指向
  • 執行階段 在這個階段進行賦值、函數引用、執行代碼。

咱們徹底能夠把執行環境看成一個含有三個屬性的對象,以下:

executionContextObj = {
    'variableObject': {...}, //函數的arguments、參數、函數內的變量及函數聲明
    'scopeChian': {...}, //本層變量對象及全部上層執行環境的變量對象
    'this': {}
}
複製代碼

聲明提高的祕密就發生在變量對象VO中。

變量對象/活動對象(VO/AO)

每一個執行環境都有一個與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個對象中。

說到執行環境的建立過程就會涉及到變量對象和活動對象,不少人對這兩個概念會模糊不清。其實,變量對象VO和活動對象AO是同一個對象在不一樣階段的表現形式。當進入執行環境的創捷階段時,變量對象被建立,這時變量對象的屬性沒法被訪問。進入執行階段後,變量對象被激活變成活動對象,此時活動對象的屬性能夠被訪問。 下面來看一下,執行環境建立階段中變量對象建立中,JavaScript解析器作的事情:

  • 根據函數參數,建立並初始化arguments對象,及形參屬性
  • 檢查上下文中的函數聲明,將函數名做爲變量對象的屬性,函數引用做爲值。若是該函數名在變量對象中已存在,則覆蓋已存在的函數引用。
  • 檢查上下文的變量聲明,將變量名做爲變量對象的屬性,值設置爲undefined。若是該變量名在變量對象中已存在,爲防止與函數名衝突,則跳過,不進行任何操做。

JavaScript中聲明提高的背後緣由已經很清晰了,你發現了嗎?請先思考一下,咱們下文將結合例子進行講解。先讓咱們結合一段代碼,結合上文的知識,回顧一下代碼執行時,會發生什麼事情。

function greet(name) {
    var say = 'hello';
    function action() {
        console.log(say + name);
    }
    action();
}
greet('vian');
複製代碼
  1. 進入全局執行環境,執行代碼。(JavaScript代碼執行時,第一個進入的老是全局執行環境)
  2. 調用函數greet(...),執行函數內的任何代碼前,建立執行環境。
  3. 進入建立階段:
    • 建立變量對象:
      • 檢查函數參數建立arguments對象,及設置函數形參。此時:
      executionContextObj = {
             arguments: {0: 'vian', length: 1},
             name: 'vian'
      }
      複製代碼
      • 掃描函數聲明,設置函數名爲變量對象的屬性,函數引用爲屬性值,遇到同名屬性則覆蓋函數引用值。此時:
      executionContextObj = {
             arguments: {0: 'vian', length: 1},
             name: 'vian',
             action: <action>
      }
      複製代碼
      • 掃描變量聲明,設置變量名爲變量對象的屬性,undefined爲屬性值,爲遇到同名屬性則跳過。此時:
      executionContextObj = {
             arguments: {0: 'vian', length: 1},
             name: 'vian',
             action: <action>,
             say: undefined
      }
      複製代碼
    • 初始化做用域鏈
    • 肯定this指向
  4. 進入執行階段,執行代碼。變量對象變成活動對象,遇到查找變量和函數引用的時候,先去活動對象中找,找不到的狀況下沿做用域鏈往上找。直至找到爲止,不然爲undefined。
  5. 函數內的代碼執行完畢,該函數執行環境出棧銷燬,程序執行流回到全局執行環境..

再看聲明提高

經過對JavaScript中執行環境的瞭解,使人奇怪的聲明提高機制也變得清晰明瞭。回到本文的開頭,形成這種聲明提高現象的本質,到底是什麼呢?——執行環境的建立階段,變量對象建立的方式所形成。下面咱們來解釋一下本文開頭的代碼。

​(function() {
    console.log(typeof foo); // 這裏會打印出什麼?
    console.log(typeof bar); // 這裏會打印出什麼?
    var foo = 'hello',
        bar = function() {
            return 'vian';
        };
    function foo() {
        return 'hello';
    }
})();​
複製代碼

根據上文中分析變量對象建立過程的方法:

executionContextObj = {}
1.初始化arguments對象,及形參
executionContextObj = {
    arguments: {length: 0}
}
2.掃描函數聲明,並進行處理:
遇到函數聲明 function foo(){}
executionContextObj中沒有foo屬性,將foo設爲executionContextObj的屬性,函數引用做爲值。
executionContextObj = {
    arguments: {length: 0},
    foo: <function>
}
3.掃描變量聲明,並進行處理:
遇到變量聲明var foo,executionContextObj已存在foo屬性,跳過。
遇到變量聲明var bar,executionContextObj不存在bar屬性,將其設置爲變量對象屬性,值爲undefined。
executionContextObj = {
    arguments: {length: 0},
    foo: <function>,
    bar: undefined
}
複製代碼

結論:

console.log(typeof foo); // 'function'
    console.log(typeof bar); // 'undefined'
複製代碼

到這裏,咱們就不再怕聲明提高的坑和問題啦。須要注意的是,es6中新增的let和const變量聲明都不會進行變量提高,重複賦值及聲明前引用變量都會報錯(TDZ)。

let const問題

var name = 'vian';
if (1) {
    name = 'em';  //TDZ開始  ReferenceError: Cannot access 'name' before initialization
    let name;     //TDZ結束  name值不會被'em'覆蓋,由於塊級做用域中使用了let聲明name,此時name
                  //綁定在了塊級做用域中,且不受外部影響。
   
}
複製代碼

到了ES6,使用新增的let、const關鍵字聲明的變量,不會按照上文的規則進行變量提高。ES6明確規定,若是區塊中存在let和const關鍵字,區塊對這些這些聲明的變量,從一開始就造成一個封閉的做用域,這些變量再也不受外部的影響(可理解爲這些變量綁定在區塊上)。在聲明以前使用這些變量,就會報錯。

總之,在代碼塊內,使用let命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱 TDZ)

但願本文對你們有所幫助,互相學習,一塊兒提升。轉載請註明原帖。

相關文章
相關標籤/搜索