代碼真的被提高了嗎?

變量提高 & 函數提高

function f() {
    console.log(a); // ?
    var a = 1;
}
f();

簡單簡單,打印結果是 undefined。爲啥?由於變量提高,變量 a 的聲明被提高到當前做用域的頂部了。也就是能夠想象成這樣:函數

function f() {
    var a;
    console.log(a);
    a = 1;
}

此外,還有函數提高,和變量提高相似:this

f1();

f2();

function f1() { console.log(1) }

function f2() { console.log(2) }

f1 和 f2 被提高了,因此不會報錯。不少文章中都會常常提到提高這個概念。
可是,這是真的嗎?JS 解釋器在執行代碼時還順帶幫你變更一下代碼的順序?想一想也以爲不太可能嘛~那麼究竟是咋回事捏?code

可執行代碼

JavaScript 的可執行代碼(executable code)有三種,分別爲 全局代碼函數代碼以及 eval 代碼。鑑於 eval 不被推薦使用,咱就不理它。對象

執行上下文

當執行全局代碼時,會建立一個全局執行上下文。當執行一個函數的時候,會建立一個函數執行上下文。全局執行上下文只會有一個,而函數執行上下文能夠有不少不少個。JS 引擎會建立 執行上下文棧(execution context stack, ECS)去管理這些執行上下文。咱們以函數執行上下文爲例。ip

變量對象

對於一個執行上下文而言,能夠抽象化爲這樣一個對象:作用域

contextObject = {
    VariableObject,
    ScopeChain,
    thisValue
}

其中變量對象(Variable Object,VO)是包含與當前執行上下文相關的數據做用域,它存儲着當前上下文中定義的變量聲明、函數聲明(注意:不包含函數表達式),另外函數執行上下文中還會有參數。io

變量對象是一個抽象概念,在不一樣的執行上下文中會以不一樣的對象呈現。好比在全局上下文中,變量對象就是全局對象自己(這也是爲何咱們可以經過全局對象的屬性名稱引用全局變量)。而對於函數執行上下文,是用活動對象(Activation Object,AO)來表示變量對象的,咱們訪問的即是活動對象上的屬性。console

執行過程

執行上下文中的代碼會被分出兩個階段進行處理,分別爲:table

  • 進入執行上下文,即分析建立階段
  • 執行代碼,即執行階段

分析建立階段

此時進入執行上下文,但在開始執行代碼以前。此時的變量對象:function

  1. 若是是函數上下文,那麼會經過函數的 arguments 屬性初始化,並處理形參。此時變量對象中會包含一個 arguments 對象,以及全部的形參,而且會檢查是否有與之對應的實參,有則值初始化爲實參的值,沒有的話就設爲 undefined。
  2. 處理函數聲明。開始處理函數聲明,再次提醒,函數表達式不會被處理。此時會檢查變量對象中是否已經有同名屬性,若是有,則替換它,若是沒有則建立屬性,值初始化爲對應的函數對象。
  3. 處理變量聲明。與處理函數聲明不一樣的是,若是變量對象中存在有同名屬性,此時會跳過該變量不作任何事,即不會產生覆蓋,若是沒有則建立屬性並初始化爲 undefined。

瞭解了第一個階段,那麼來看個例子:

function f(a, b) {
    var c = 3;
    function d() {};
    var e = function() {};
    (function f() {});
}

f(1, 2);

當執行 f 函數時,進入執行上下文,咱們能夠描述出此時的 AO:

AO = {
    arguments: {
        0: 1,
        1: 2,
        length: 2
    },
    a: 1,
    b: 2,
    c: undefined,
    d: reference to function() {},
    e: undefined,
}
// 注意:(function f() {}) 爲函數表達式,不會被處理

執行階段

執行階段就相對簡單,代碼順序執行,而且會根據代碼去修改變量對象中的值,因此當函數執行完後的 AO 會是:

AO = {
    arguments: {
        0: 1,
        1: 2,
        length: 2
    },
    a: 1,
    b: 2,
    c: 3,
    d: reference to function() {},
    e: reference to function() {},
}

總結

綜上所述:

  1. 全局上下文的變量對象便是全局對象;
  2. 函數上下文的變量對象會以 arguments 對象初始化;
  3. 在第一個階段會依次給變量對象添加形參(函數上下文)、函數聲明、變量聲明;
  4. 在第二個階段,會修改變量對象中的屬性值。

思考題

console.log(f); // ?

function f() {}
var f = 1;

打印結果會是一個函數對象。由於第一個階段中先處理函數聲明後處理變量聲明,當處理變量聲明時變量對象中已經存在同名屬性 f,不會作任何事直接跳過,因此 f 的值會是一個函數。

你也能夠多找幾道題嘗試分析以加深理解。固然,平常工做中仍是直接想成提高來得方便些,只是但願經過本文能讓你瞭解到一些背後的事情。最後,若是文中有任何錯誤或不足之處,還請指出~

相關文章
相關標籤/搜索