先來兩個問題 很多時候,在直覺上,我們都會認為JS代碼在執行時都是自上而下一行一行執行的,但是實際上,有一種情況會導致這個假設是錯誤的。 按照傳統眼光,console.log(a)輸出的應該是undefined,因為var a在a = 2之後。但是,輸出的是2。 再看第二段代碼: 有人會想到第一段代 ...
先來兩個問題
很多時候,在直覺上,我們都會認為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