新手秒懂 - 高逼格解釋變數提升

夏天Summer發表於2019-06-21

作者廢話

最近為了未來去大城市面試特意重新穩固基礎知識

面試多了也會發現,很多都會涉及到平時工作中不去關心的問題, 接來下會不定時地像同樣的朋友們分享平常工作中不會接觸卻又常被問到的面試知識點

變數提升?

這個問題試問剛畢業的前端小白都能侃侃而談,我們以兩道很常見的又很基礎的面試題一步一步揭開它的面紗。(作為初學者的我經常搞混)

    function fun () {}
    var fun = 'fuck bitch'
    console.log(fun) //???
複製程式碼
    console.log(fun) //???
    function fun () {}
    var fun = 'fuck bitch'
複製程式碼

大佬都會說,太簡單了。

  1. 首先,第一題輸出的是funck bitch,這不涉及變數提升,只是同名的變數產生了覆蓋。(注意: 這發生在執行階段)
  2. 然後,第二道題, 輸出ƒ () {} , 具體的原因,會談到大家都知道的變數提升知識

我們習慣將 var a = 2; 看作一個宣告,而實際上 JavaScript 引擎並不這麼認為。它將 var a 和 a = 2 當作兩個單獨的宣告,第一個是編譯階段的任務,而第二個則是執行階段的任務。

這意味著無論作用域中的宣告出現在什麼地方,都將在程式碼本身被執行前 首先 進行處理。 可以將這個過程形象地想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個過程被稱為提升。

宣告本身會被提升,而包括函式表示式的賦值在內的賦值操作並不會提升。

參考地址

如此可以解釋 fun 沒有報錯的原因,但很多跟我一樣的初學者就會問為何輸出 ƒ () {} 而不是 undefined 或者 fuck bitch

對於這樣的問題,按照我之前的理解。在我眼裡,執行程式碼其實就是

function fun () {}
console.log(fun) // --> fun(){}
fun = 'fuck bitch'
複製程式碼

但如果問我為啥會是這樣的,我就說不出來個所以然了 (果然還是太菜了。。)

為何會產生變數提升??

不知有沒有同學想過這樣的問題,本來一般只是為了應付面試而去瞄一眼說出個所以然就可以的。但作為要成為未來資深的禿頭披風大佬,這樣做是遠遠不夠的。

這就涉及到javascript語言中執行上下文之變數物件的知識了

image

當 JavaScript 程式碼執行一段可執行程式碼(executable code)時,會建立對應的執行上下文(execution context)。

image

一個執行上下文的生命週期可以分為兩個階段。

  1. 建立階段

在這個階段中,執行上下文會分別建立變數物件,建立作用域鏈,以及確定this的指向。

  1. 程式碼執行階段

建立完成之後,就會開始執行程式碼,這個時候,會完成變數賦值,函式引用,以及執行其他程式碼。

變數物件(Variable Object)

變數物件的建立,依次經歷了以下幾個過程。

  1. 建立arguments物件。檢查當前上下文中的引數,建立該物件下的屬性與屬性值。
  2. 檢查當前上下文的函式宣告,也就是使用function關鍵字宣告的函式。在變數物件中以函式名建立一個屬性,屬性值為指向該函式所在記憶體地址的引用。如果函式名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。(常說的函式優先被提升, 且同名會產生覆蓋)
  3. 檢查當前上下文中的變數宣告(未宣告的會報錯 not defined),每找到一個變數宣告,就在變數物件中以變數名建立一個屬性,屬性值為undefined。如果該變數名的屬性已經存在,為了防止同名的函式被修改為undefined,則會 直接跳過 (注意: 跳過是在建立階段,跟第一題相對應,不要搞混),原屬性值 不會被修改

對照兩道題,結合描述,我們就可以瞭解到具體執行原理:

    function fun () {}
    var fun = 'fuck bitch'
    console.log(fun) // 'fuck bitch'
複製程式碼

因為 fun = 'fuck bitch' 是在執行上下文的執行過程中執行的,而不會產生覆蓋,跳過操作是在建立階段,因此輸出結果自然會是fuck bitch

而第二道題

    console.log(fun) // fun () {}
    function fun () {}
    var fun = 'fuck bitch'
複製程式碼

因為 function 是優先被提升, 而接下來的變數 fun 因為同名而不會在建立階段產生覆蓋,所以輸出 fun () {}。 具體如下圖相同。

// 上例的執行順序為

// 首先將所有函式宣告放入變數物件中
function fun () {}

// 其次將所有變數宣告放入變數物件中,但是因為fun已經存在同名函式,因此此時會跳過undefined的賦值
// var fun = undefined;

// 然後開始執行階段程式碼的執行
console.log(fun); // function fun
fun = 'fuck bitch';
複製程式碼

這樣一解釋,是不是覺得自己逼格瞬間上升了一個檔次 ?!

深入擴充

還不用太興奮,我們為了加深理解,換個例子繼續一步一步來詳細地講。

function test() {
    console.log(a);
    console.log(foo());

    var a = 1;
    function foo() {
        return 2;
    }
}

test();
複製程式碼

VO AND AO

在函式上下文, 我們使用活動物件(activation object, AO)來表示變數物件。

未進入執行階段之前,變數物件(VO)中的屬性都不能訪問!但是進入執行階段之後,變數物件(VO)轉變為了活動物件(AO),裡面的屬性都能被訪問了,然後開始進行執行階段的操作。

它們其實都是同一個物件,只是處於執行上下文的不同生命週期。

按照上例:
// 建立過程
testEC = {
    // 變數物件
    VO: {},
    scopeChain: {}
}

AO = {
    arguments: {...},  //注:在瀏覽器的展示中,函式的引數可能並不是放在arguments物件中,這裡為了方便理解,我做了這樣的處理
    foo: <foo reference>  // 表示foo的地址引用
    a: undefined
}
複製程式碼
// 執行階段
VO ->  AO   // Activation Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}
複製程式碼

按此來說, 上例的執行順序應該是:

function test() {
    function foo() {
        return 2;
    }
    var a = undefined;
    console.log(a);
    console.log(foo());
    a = 1;
}
test();
複製程式碼

接下來增加難度:

// demo2
function test() {
    console.log(1, foo);
    console.log(2, bar);

    var foo = 'Hello';
    console.log(3, foo);
    var bar = function () {
        return 'world';
    }

    function foo() {
        return 'hello';
    }
}

test(); // ??? 
複製程式碼

可轉換為

// demo2
function test() {
    function foo() {
        return 'hello';
    }
    // 其次將所有變數宣告放入變數物件中,但是因為foo已經存在同名函式,因此此時會跳過undefined的賦值
    // var foo = undefined;
    
    var bar = undefined;
    
    console.log(1, foo);
    console.log(2, bar);

    var foo = 'Hello';
    
    console.log(3, foo);
    
    bar = function () {
        return 'world';
    }
}

test(); 
// 1 ƒ foo() {
// return 'hello';
// }
// 2 undefined
// 3 "Hello"
複製程式碼

到此,便是我想向大家分享的內容。努力,奮鬥。??

參考文獻

變數物件詳解

深入之變數物件

相關文章