# 第四章:提升
至此,你應當對作用域的想法,以及變量如何根據它們被聲明的方式和位置附著在不同的作用域層級上感到相當適應了。函數作用域和塊兒作用域的行為都是依賴于這個相同規則的:在一個作用域中聲明的任何變量都附著在這個作用域上。
但是關于出現在一個作用域內各種位置的聲明如何附著在作用域上,有一個微妙的細節,而這個細節正是我們要在這里檢視的。
## 先有雞還是先有蛋?
有一種傾向認為你在 JavaScript 程序中看到的所有代碼,在程序執行的過程中都是從上到下一行一行地被解釋執行的。雖然這大致上是對的,但是這種猜測中的一個部分可能會導致你錯誤地考慮你的程序。
考慮這段代碼:
```source-js
a = 2;
var a;
console.log( a );
```
你覺得在?`console.log(..)`?語句中會打印出什么?
許多開發者會期望?`undefined`,因為語句?`var a`?出現在?`a = 2`?之后,這很自然地看起來像是這個變量被重定義了,并因此被賦予了默認的?`undefined`。然而,輸出將是?`2`。
考慮另一個代碼段:
```source-js
console.log( a );
var a = 2;
```
你可能會被誘導而這樣認為:因為上一個代碼段展示了一種看起來不是從上到下的行為,也許在這個代碼段中,也會打印?`2`。另一些人認為,因為變量?`a`?在它被聲明之前就被使用了,所以這一定會導致一個?`ReferenceError`?被拋出。
不幸的是,兩種猜測都不正確。輸出是?`undefined`。
那么。這里發生了什么??看起來我們遇到了一個先有雞還是先有蛋的問題。哪一個先有?聲明(“蛋”),還是賦值(“雞”)?
## 編譯器再次襲來
要回答這個問題,我們需要回頭引用第一章關于編譯器的討論。回憶一下,*引擎*?實際上將會在它解釋執行你的 JavaScript 代碼之前編譯它。編譯過程的一部分就是找到所有的聲明,并將它們關聯在合適的作用域上。第二章向我們展示了這是詞法作用域的核心。
所以,考慮這件事情的最佳方式是,在你的代碼的任何部分被執行之前,所有的聲明,變量和函數,都會首先被處理。
當你看到?`var a = 2;`?時,你可能認為這是一個語句。但是 JavaScript 實際上認為這是兩個語句:`var a;`?和?`a = 2;`。第一個語句,聲明,是在編譯階段被處理的。第二個語句,賦值,為了執行階段而留在?原處。
于是我們的第一個代碼段應當被認為是這樣被處理的:
```source-js
var a;
```
```source-js
a = 2;
console.log( a );
```
……這里的第一部分是編譯,而第二部分是執行。
相似地,我們的第二個代碼段實際上被處理為:
```source-js
var a;
```
```source-js
console.log( a );
a = 2;
```
所以,關于這種處理的一個有些隱喻的考慮方式是,變量和函數聲明被從它們在代碼流中出現的位置“移動”到代碼的頂端。這就產生了“提升”這個名字。
換句話說,先有蛋(聲明),后有雞(賦值)。
注意:?只有聲明本身被提升了,而任何賦值或者其他的執行邏輯都被留在?*原處*。如果提升會重新安排我們代碼的可執行邏輯,那就會是一場災難了。
```source-js
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
```
函數?`foo`?的聲明(在這個例子中它還?*包含*?一個隱含的、實際為函數的值)被提升了,因此第一行的調用是可以執行的。
還需要注意的是,提升是?以作用域為單位的。所以雖然我們的前一個代碼段被簡化為僅含有全局作用域,但是我們現在檢視的函數`foo(..)`本身展示了,`var a`被提升至`foo(..)`的頂端(很明顯,不是程序的頂端)。所以這個程序也許可以更準確地解釋為:
```source-js
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
```
函數聲明會被提升,就像我們看到的。但是函數表達式不會。
```source-js
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
```
變量標識符?`foo`?被提升并被附著在這個程序的外圍作用域(全局),所以?`foo()`?不會作為一個?`ReferenceError`?而失敗。但?`foo`?還沒有值(如果它不是函數表達式,而是一個函數聲明,那么它就會有值)。所以,`foo()`?就是試圖調用一個?`undefined`?值,這是一個?`TypeError`?—— 非法操作。
同時回想一下,即使它是一個命名的函數表達式,這個名稱標識符在外圍作用域中也是不可用的:
```source-js
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
```
這個代碼段可以(使用提升)更準確地解釋為:
```source-js
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}
```
## 函數優先
函數聲明和變量聲明都會被提升。但一個微妙的細節(*可以*?在擁有多個“重復的”聲明的代碼中出現)是,函數會首先被提升,然后才是變量。
考慮這段代碼:
```source-js
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
```
`1`?被打印了,而不是?`2`!這個代碼段被?*引擎*?解釋執行為:
```source-js
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
```
注意那個?`var foo`?是一個重復(因此被無視)的聲明,即便它出現在?`function foo()...`?聲明之前,因為函數聲明是在普通變量之前被提升的。
雖然多個/重復的?`var`?聲明實質上是被忽略的,但是后續的函數聲明確實會覆蓋前一個。
```source-js
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}
```
雖然這一切聽起來不過是一些有趣的學院派細節,但是它強調了一個事實:在同一個作用域內的重復定義是一個十分差勁兒的主意,而且經常會導致令人困惑的結果。
在普通的塊兒內部出現的函數聲明一般會被提升至外圍的作用域,而不是像這段代碼暗示的那樣有條件地被定義:
```source-js
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log( "a" ); }
}
else {
function foo() { console.log( "b" ); }
}
```
然而,重要的是要注意這種行為是不可靠的,而且是未來版本的 JavaScript 將要改變的對象,所以避免在塊兒中聲明函數可能是最好的做法。
## 復習
我們可能被誘導而將?`var a = 2`?看作是一個語句,但是 JavaScript?*引擎*?可不這么看。它將?`var a`?和?`a = 2`?看作兩個分離的語句,第一個是編譯期的任務,而第二個是執行時的任務。
這將導致在一個作用域內的所有聲明,不論它們出現在何處,都會在代碼本身被執行前?*首先*?被處理。你可以將它可視化為聲明(變量與函數)被“移動”到它們各自的作用域頂部,這就是我們所說的“提升”。
聲明本身會被提升,但不是賦值,即便是函數表達式的賦值,也?*不會*?被提升。
要小心重復聲明,特別是將一般的變量聲明和函數聲明混在一起 —— 如果你這么做的話,危險就在眼前!