淺談JS變數宣告和函式宣告提升

格子熊發表於2018-04-30

先來兩個問題

很多時候,在直覺上,我們都會認為JS程式碼在執行時都是自上而下一行一行執行的,但是實際上,有一種情況會導致這個假設是錯誤的。

a = 2;
var a;
console.log(a);

按照傳統眼光,console.log(a)輸出的應該是undefined,因為var a在a = 2之後。但是,輸出的是2。

再看第二段程式碼:

console.log(a);
var a = 2;

有人會想到第一段程式碼,然後回答undefined。還有人會認為a在使用前未被宣告,因此丟擲ReferenceError異常。遺憾的是,結果是undefined。

為什麼呢?

從編譯器的角度看問題

JS在編譯階段,編譯器的一部分工作就是找到所有宣告,並用合適的作用域將他們關聯起來。對於一般人來說var a = 2僅僅是一個宣告,但是,JS編譯器會將該段程式碼拆為兩段,即:var a和a = 2。var a這個定義宣告會在編譯階段執行,而a = 2這個賦值宣告會在原地等待傳統意義上的從上到下的執行。

所以,在編譯器的角度來看,第一段程式碼實際上是這樣的:

var a;  // 編譯階段執行
a = 2;
console.log(a);

所以,輸出的是2。

類似的,第二個程式碼片段實際上是這樣執行的:

var a;
console.log(a);
a = 2;

這樣的話,很明顯,輸出的應該是undefined,因為只對a進行了定義宣告,沒有對a進行賦值宣告。

從上面這兩個例子可以看出,變數宣告會從它們在程式碼中出現的位置被移動到當前作用域的最上方進行執行,這個過程叫做提升

函式提升

下面,再來看一段程式碼

foo();

function foo () {
    console.log(a);
    var a = 2;
}

在這個例子中,輸出undefined而不會報錯,因為,函式變數也能提升。即,實際上像如下的情況執行。

function foo () {
    var a;
    console.log(a);
    a = 2;
}

foo();

說到這裡,你是不是認為提升很簡單,只要把變數都放到當前作用域最上方執行就好了?

下面,我來說一種意外情況:函式表示式的提升情況。

函式表示式的提升情況

foo();

var foo = function bar () {
    console.log(a);
    var a = 2;
}

你是不是想說,這個例子不是和之前的那個差不多嗎?輸出的當然是undefined呀。但是,結果是,不輸出,因為JS報了TypeError錯誤!

因為,函式表示式不會進行提升!

該例子的實際執行情況是這樣的:

var foo;
foo();
foo = function bar () {
    var a;
    console.log(a);
    a = 2;
}

由於執行時,在作用域中找得到foo(該作用域最上方宣告瞭foo),所以不會報ReferenceError錯誤,但是,foo此時沒有進行賦值(如果foo是一個函式宣告而不是函式表示式,那麼就會賦值),也就是說實際上foo()是對一個值為undefined的變數進行函式呼叫,所以,理所應當丟擲TypeError異常。

值得一提的是,即使是具名的函式表示式,名稱識別符號在賦值之前也無法在所在作用域中使用,即:

foo();  // TypeError
bar();  // ReferenceError

var foo = function bar () {}

函式優先

函式宣告和變數宣告都會被提升,但是有一個值得注意的細節,那就是,函式會首先提升,然後才是變數!

看下面這一段程式碼:

foo();
var foo;
function foo () {
    console.log(1);
}
foo = function () {
    console.log(2);
}

這一段程式碼會輸出1,原因就在於,函式優先。

這一段程式碼可以轉換為以下形式:

function foo () {
    console.log(1);
}
var foo;    // 重複宣告,被忽略
foo();      // 輸出1
foo = function () {
    console.log(2);
}

如果,在程式碼的結尾再執行一次foo函式,此時,輸出的是1。

function foo () {
    console.log(1);
}
var foo;    // 重複宣告,被忽略
foo();      // 輸出1
foo = function () {
    console.log(2);
}
foo();      // 輸出2

因為,儘管重複的宣告會被忽略了,但是後面的函式還是可以覆蓋前面的函式。

明白了這個道理,你就可以理解下面這個問題了:

foo();
var a = true;
if (a) {
    function foo () {
        console.log("a");
    }
} else {
    function foo () {
        console.log("b");
    }
}

你猜這道題輸出的結果是什麼?是b!為什麼?因為foo進行了兩次的宣告,但是,後一次函式覆蓋了前一次的函式。所以呼叫foo時,永遠呼叫的都是console.log(“b”)。

總結

1.所有宣告(變數和函式)都會被移動到各自作用域的最頂端,這個過程被稱為提升

2.函式表示式等各種賦值操作並不會被提升

3.函式優先原則

4.儘量避免產生提升問題

參考資料:You Dont`t Know JS: SCope & Closures

相關文章