你知道下面的JavaScript代碼執(zhí)行時(shí)會(huì)輸出什么嗎?
var foo = 1;
function bar() {
if (!foo) {
var foo = 10;
}
alert(foo);
}
bar();
答案是“10”,吃驚嗎?那么下面的可能會(huì)真的讓你大吃一驚:
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
alert(a);
這里瀏覽器會(huì)彈出“1”。怎么回事?這似乎看起來(lái)是奇怪,未知,讓人混淆的,但這實(shí)際上是這門(mén)語(yǔ)言一個(gè)強(qiáng)大和富有表現(xiàn)力的特性。我不知道這一特性行為是否有標(biāo)準(zhǔn)名字,但我喜歡這個(gè)術(shù)語(yǔ)“提升(hoisting)”。本文試圖揭示這一特性的機(jī)制,但首先讓我們鏈接JavaScript的作用域。
JavaScript初學(xué)者最容易混淆的地方是作用域。實(shí)際上,不只是初學(xué)者。我遇到過(guò)許多經(jīng)驗(yàn)豐富的JavaScript程序員,卻不完全明白作用域。JavaScript的作用域如此容易混淆的原因是它看起來(lái)很像C家族的語(yǔ)言(類(lèi)C語(yǔ)言)??紤]下面的C程序:
#include <stdio.h>
int main() {
int x = 1;
printf("%d, ", x); // 1
if (1) {
int x = 2;
printf("%d, ", x); // 2
}
printf("%d\n", x); // 1
}
程序的輸出是1,2,1.這是因?yàn)镃和C家族的語(yǔ)言有塊級(jí)作用域(block-level scope)。當(dāng)控制流進(jìn)入一個(gè)塊,比如if語(yǔ)句,新的變量會(huì)在塊作用域里聲明,不會(huì)對(duì)外面作用域產(chǎn)生印象。這不適用于JavaScript。在Firebug里運(yùn)行下面的代碼:
var x = 1;
console.log(x); // 1
if (true) {
var x = 2;
console.log(x); // 2
}
console.log(x); // 2
在這個(gè)例子中,F(xiàn)irebug將輸出1,2,2。這是因?yàn)镴avaScript有函數(shù)級(jí)作用域(function-level scope)。這一點(diǎn)和C家族完全不同。語(yǔ)句塊,如if語(yǔ)言,不創(chuàng)建新的作用域。僅僅函數(shù)創(chuàng)建新作用域。
很多程序員,像C,C++,C#或Java,都不知道這點(diǎn),也不希望這樣。幸運(yùn)的是,因?yàn)镴avaScript函數(shù)的靈活性,有一個(gè)解決方案。你若你必須要在函數(shù)內(nèi)部創(chuàng)建一個(gè)臨時(shí)作用域,像下面這樣做:
function foo() {
var x = 1;
if (x) {
(function () {
var x = 2;
// 此處省略一萬(wàn)個(gè)字
}());
}
// x 仍然是 1.
}
這方法實(shí)際上相當(dāng)靈活,可以在你需要臨時(shí)作用域的時(shí)候隨意使用,不局限于塊級(jí)語(yǔ)句內(nèi)部。然而,我強(qiáng)烈建議你花時(shí)間去了解和欣賞JavaScript的作用域。它非常強(qiáng)大,是這門(mén)語(yǔ)言中我最喜歡的特性之一。如果你了解作用域,將更容易理解提升。
在JavaScript中,作用域中的名字(屬性名)有四種基本來(lái)源:
變量聲明:var foo;這種形式的代碼。 函數(shù)聲明和變量聲明總是被JavaScript解釋器無(wú)形中移動(dòng)到(提升)包含他們的作用域頂部。函數(shù)參數(shù)和語(yǔ)言定義的名稱(chēng)明顯總是存在。這意味著像下面的代碼:
function foo() { bar(); var x = 1; }
實(shí)際上被解釋為像下面這樣:
function foo() {
var x;
bar();
x = 1;
}
無(wú)論包含聲明的代碼行是否會(huì)被執(zhí)行,上面的過(guò)程都會(huì)發(fā)生。下面的兩個(gè)函數(shù)是等價(jià)的:
function foo() {
if (false) {
var x = 1;
}
return;
var y = 1;
}
function foo() {
var x, y;
if (false) {
x = 1;
}
return;
y = 1;
}
注意變量聲明中賦值的過(guò)程不會(huì)被提升。僅僅變量名字被提升了。這不適用于函數(shù)聲明,整個(gè)函數(shù)體也會(huì)提升。但不要忘記有兩種聲明函數(shù)的方法。考慮下面的JavaScript代碼:
function test() {
foo(); // 類(lèi)型錯(cuò)誤 “foo 不是一個(gè)函數(shù)”
bar(); // “這能運(yùn)行”
var foo = function () { // 將函數(shù)表達(dá)式賦值給本地變量“foo”
alert("this won't run!");
}
function bar() { // 'bar'函數(shù)聲明,分配“bar”名字
alert("this will run!");
}
}
test();
在這種情況下,僅僅函數(shù)聲明的函數(shù)體被提升到頂部。名字“foo”被提升,但后面的函數(shù)體,在執(zhí)行的時(shí)候才被指派。
這是全部的基本提升,看起來(lái)并不復(fù)雜和讓人混淆。當(dāng)然,這是JavaScript,在某些特殊性況下會(huì)更復(fù)雜一點(diǎn)。
需要記住的最重要的特殊情況是名字的解析順序。記住作用域中的名字有四種來(lái)源。上面我列出他們的順序是他們被解析的順序。一般來(lái)說(shuō),如果一個(gè)名字已經(jīng)被定義過(guò),那么它不會(huì)在被其他有相同名字的屬性重寫(xiě)。這意味著函數(shù)聲明優(yōu)先于變量聲明。這并不意味著為名字賦值的過(guò)程將不工作,僅僅聲明的過(guò)程會(huì)被忽略。有幾個(gè)例外情況:
你可以在函數(shù)表達(dá)式給中給函數(shù)命名,用這樣的語(yǔ)法不能完成一個(gè)函數(shù)聲明,下面有一些代碼來(lái)說(shuō)明我的意思:
foo(); // TypeError "foo is not a function"
bar(); // valid
baz(); // TypeError "baz is not a function"
spam(); // ReferenceError "spam is not defined"
var foo = function () {}; // 匿名函數(shù)表達(dá)式(“foo”會(huì)被提升)
function bar() {}; // 函數(shù)聲明(“bar”和函數(shù)體會(huì)被提升)
var baz = function spam() {}; // 命名函數(shù)表達(dá)式(僅“baz”會(huì)被提升)
foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"
現(xiàn)在你應(yīng)該理解了作用域和提升(hoisting),那么我們?cè)诰帉?xiě)JavaScript的時(shí)候應(yīng)該怎么做呢?最重要的事情就是始終用var表達(dá)式來(lái)聲明你的變量。我強(qiáng)烈建議你使用單var模式(single var)。如果你強(qiáng)迫自己做到這一點(diǎn),你將永遠(yuǎn)不會(huì)遇到任何與變量提升相關(guān)的混亂的問(wèn)題。但是這樣做也讓我們很難跟蹤那些在當(dāng)前作用域中實(shí)際上已經(jīng)聲明的變量。我建議你使用JSLint和聲明一次原則來(lái)進(jìn)行實(shí)際操作,如果你這樣做了,你的代碼應(yīng)該會(huì)看起來(lái)像這樣:
/*jslint onevar: true [...] */
function foo(a, b, c) {
var x = 1,
bar,
baz = "something";
}
我翻了翻ECMAScript標(biāo)準(zhǔn),想直接了解這些東西是如何工作的,發(fā)現(xiàn)效果不錯(cuò)。這里我不得不說(shuō)關(guān)于變量聲明和作用域(第12.2.2節(jié))的內(nèi)容:
如果在一個(gè)函數(shù)中聲明變量,這些變量就被定義在了在該函數(shù)的函數(shù)作用域中,見(jiàn)第10.1.3所述。不然它們就是被定義在全局的作用域內(nèi)(即,它們被創(chuàng)建為全局對(duì)象的成員,見(jiàn)第10.1.3所述),當(dāng)進(jìn)入執(zhí)行環(huán)境的時(shí)候,變量就被創(chuàng)建。一個(gè)語(yǔ)句塊不能定義一個(gè)新的作用域。只有一個(gè)程序或者函數(shù)聲明能夠產(chǎn)生一個(gè)新的作用域。創(chuàng)建變量時(shí),被初始化為undefined。如果變量聲明語(yǔ)句里面帶有賦值操作,則賦值操作只有被執(zhí)行到聲明語(yǔ)句的時(shí)候才會(huì)發(fā)生,而不是創(chuàng)建的時(shí)候。
我希望這篇文章闡明了對(duì)JavaScript程序員來(lái)說(shuō)最常見(jiàn)的迷惑問(wèn)題,我試圖講的盡可能詳盡,以避免造成更多的迷惑,如果我說(shuō)錯(cuò)了或者有大的遺漏,請(qǐng)通知我。
原文 http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html
更多建議: