學習JavaScript中的“提升”

YaeSakura發表於2019-04-21

先有雞還是先有蛋

通過之前的文章,我們熟悉了作用域的基本概念。但是作用域中的變數,函式宣告在什麼地方查詢,引用它們的時候又發生了什麼。正是我們將要討論的內容。

在我們的認知中JavaScript程式碼在執行的時候是由上到下一行一行執行的。但實際上並不完全正確。例如:

a = 1;
var a;
console.log(a);
複製程式碼

按照我們之前的認知由上到下,最後a輸出undefined,因為var a宣告在a = 1後面,但最後輸出的結果是1

考慮另外一段程式碼:

console.log(a);
var a = 1;
複製程式碼

鑑於上一個程式碼片段所表現的特點,可能認為這個程式碼片段也會輸出1,或者可能丟擲異常錯誤。實際上輸出的是undefined

那麼到底是宣告在前,還是賦值在前?

回顧JavaScript引擎

為了弄明白這個問題,我們需要再次回顧JavaScript引擎,引擎會在解釋JavaScript程式碼之前首先對其進行編譯。編譯階段中的一個很重要的工作就是找到所有的宣告,並在合適的作用域中將它們關聯起來。

執行環境

執行環境也可以叫執行上下文,每當JavaScript編譯器工作時,都會建立一個執行環境或者說進入一個執行上下文中。它們定義了變數或函式訪問其他資料的許可權,決定了它們各自的行為。它們在邏輯上組成一個堆疊,堆疊底部永遠是全域性環境,而頂部就是當前環境。

例如:我們可以定義執行環境是一個陣列:

stack = [];
複製程式碼

在初始化階段,stack是這樣的:

stack = [
    globalContext
];
複製程式碼

每次函式執行,進入function的時候,這個堆疊都會被壓入。

function foo(){
    return 'hello';
}
foo();
複製程式碼

那麼,stack將會發生改變:

stack = [
    <foo> functionContext
    globalContext
];
複製程式碼

每次函式退出也就是執行到return的時候,都會退出當前的執行環境,相應的stack就會彈出,棧中的指標會移動位置。相關程式碼執行完畢後,stack只會包含全域性環境,一直到整個程式結束。

變數物件

在進行JavaScript程式設計是總避免不了宣告函式和變數,在每個執行環境中有一個變數物件,我們定義的所有變數和函式都儲存在這個物件中。

變數物件(VO)儲存一下內容:

函式宣告(function)

變數宣告(var)

我們可以用一個JavaScript物件來表示一個變數物件例如:

VO = {};
複製程式碼

如前面所說執行環境中有一個變數物件,它是執行環境的一個屬性,例如:

context = {
    VO = {};
}
複製程式碼

當我們宣告一個變數或一個函式的時候,例如:

var a = 1;
 
function foo() {
  var b = 20;
};
 
test();
複製程式碼

對應的變數物件是:

//全域性環境的變數物件
globalContext: {
    vo: {
        a: 1,
        foo: function
    }
}
//foo函式環境的變數物件
fooContext: {
    vo: {
        b: 20
    }
}
複製程式碼

函式宣告和變數宣告會被提升

現在終於到了本文的核心點了,當我們的程式碼執行時,首先在執行環境的變數物件中宣告變數和函式,然後才是程式碼執行階段。當我們看到var a = 1時,實際上JavaScript會將其看成兩部分:var = aa = 1

var a;
a = 1;
複製程式碼

函式優先

函式宣告會首先被提升,然後才是變數,例如:

foo(); // 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 );
};
複製程式碼

小結

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

參考

相關文章