淺談JS變量聲明和函數聲明提升


先來兩個問題

很多時候,在直覺上,我們都會認為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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM