你知道如下的JavaScript代碼被執行后,會彈出什么?
var foo = 1;
function bar() {
? ? if (!foo) {
? ? var foo = 10;
? ? }
? ? alert(foo);
}
bar();
如果你對彈出的結果是“10”感到驚訝,下面的這段代碼彈出的結果會讓你感到震驚。
var a = 1;
function b() {
? ? a = 10;
? ? return;
? ? function a() {}
}
b();
alert(a);
當然,上面的代碼會讓瀏覽器彈出“1”。那么這中間究竟發生了什么?雖然這看起來似乎讓人感到陌生,危險,困惑,但是這就是JavaScript語言的強大并富有表現力的特征。我不知道對這個特殊的行為是否有標準的名稱,但是我喜歡用“hoisting”來標識它。這邊文章將會嘗試揭示為什么會這樣,但是我們先要繞個路,來了解下JavaScript的作用域(scoping)。
JavaScript中的作用域(scoping)
對于JavaScript初學者來說最讓人困惑的來源之一就是作用域(scoping)。事實上,不僅是初學者,我也遇到許多有經驗的JavaScript程序員,他們也不是完全了解作用域。在JavaScript中的作用域是如此的讓人感到困惑,究其原因是JavaScript看起來像是C家族的語言。考慮下列的C程序:
#include
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”。之所以輸出這樣的結果是因為C和其它的C家族語言都有著“block-level”作用域。當 控制(control)進入block(比如if聲明)后,在if的作用域中就可以聲明新的變量,而不影響外層的作用域。但是這卻不適用于JavaScript。在Firebug中測試如下的代碼:
var x = 1;
console.log(x); // 1
if (true) {
? ? var x = 2;
? ? console.log(x); // 2
}
console.log(x); // 2
在這個例子中,Firebug將會顯示“1,2,2”。這是因為JavaScript中只有function-level(函數作用域)。這就是和C語言的區別。Blocks(比如if聲明)不會創建一個新的作用域。只有函數才會創建新的作用域。
對于許多熟悉C,C++,C#,Java的程序員來說,這是出乎意料的和不收歡迎的。值得慶幸的是,由于JavaScript中函數的靈活性,可以找到一個變通方法。如果你一定要在函數中創建一個臨時的作用域,可以嘗試像下面這樣做:
function foo() {
? ? var x = 1;
? ? if (x) {
? ? ? ? (function () {
? ? ? ? ? ? var x = 2;
? ? ? ? ? ? // some other code
? ? ? ? }()); ? ?//(function(){}())
? ? } ? ?//if(x)
// x is still 1.
}
事實上,這個方法非常靈活,可以在任何你需要臨時作用域的地方進行使用,不僅僅是在block聲明之內。然而,我強烈建議你花點時間來理解下JavaScript的作用域。它是如此的強大,并且是我喜愛的語言特征之一。如果你理解了作用域,hoisting(提前)對你來說會好理解許多。
聲明,命名,和Hoisting
在JavaScript中,一個名字可以用四種方式中的其中之一進入作用域:
Language-defined: ?默認情況下,所有的函數作用域都被傳遞了this和arguments這2個參數。
Formal parameters(作為形參): 就像其它語言中的形參那樣。
Function declarations(函數聲明):函數聲明具有function foo() {}這樣的形式。
Variable declarations(變量聲明):變量聲明采取var foo這樣的形式。
函數聲明和變量聲明被JavaScript的interpreter(解釋器)隱式的移動到它們作用域的頂部。函數形參和Language-difined(語言定義的)名字 很明顯已經在頂部了。這意味著像這樣的代碼:
function foo() {
? ? bar();
? ? var x = 1;
}
被解釋為這樣:
function foo() {
? ? var x;
? ? bar();
? ? x = 1;
}
不管包含聲明的那行代碼是否會被執行。如下的2個函數式等價的:
function foo() {
? ? if (false) {
? ? ? ? var x = 1;
? ? }
? ? return;
? ? var y = 1;
}
function foo() {
? ? var x, y;
? ? if (false) {
? ? ? ? x = 1;
? ? }
? ? return;
? ? y = 1;
}
注意聲明的賦值部分并沒有被hoisted,只有聲明部分被hoisted。這不同于函數聲明(函數聲明會將整個函數體也hoist)。但是要記得有2種常用方式來聲明函數。思考如下的JavaScript代碼:
function test() {
? ? foo(); // TypeError "foo is not a function"
? ? bar(); // "this will run!"
? ? var foo = function () { // function expression assigned to local variable 'foo'
? ? ? ? alert("this won't run!");
? ? }//var foo = function(){}
? ? function bar() { // function declaration, given the name 'bar'
? ? ? ? alert("this will run!");
? ? }// ?function bar() {} ?
}
test();
在這個例子中,bar的函數聲明及其函數體被提前到頂部。變量foo的聲明被提前,但是其右側的匿名函數及其函數體并沒有提前,被留下來 ?等待在執行時賦值給foo。
上述闡述覆蓋了hoisting的基本情況,事實上并不像看起來的那樣復雜。當然,JavaScript中的this指針,在某些特殊的場合下,是有點復雜的。
Name Resolution Order(名稱解析順序)
需要謹記的最重要的特殊情況是name resolution order。有4種方式供名稱進入給定的作用域。我列出它們的順序就是它們被解析的順序。總的來說,如果一個名稱已經被定義了,它不會被另一個同名的property覆蓋。這意味著函數聲明的優先級高于變量聲明。這并不意味著對那個名稱的賦值會不起作用,僅僅是(=右邊的)聲明部分會被忽略。
這兒有一些例外:bulit-in(內建的)arguments 舉止有些古怪。它似乎是在形參后聲明的,但是在函數聲明前。這意味著如果形參的名稱被取為arguments,那么它的優先級高于內建的arguments,即使它是undefined。這是個不好的特性,所以不要形參不要命名為arguments。
嘗試使用this作為標識符會導致SyntaxError(語法錯誤)。這是個好的特性。
如果多個形參的名字相同的話,最后出現的那個會高于其它的,即使它是undefined。
Named Function Expressions(有名函數表達式)
你可以在函數表達式中給定義的函數一個名字(使用類似函數聲明的語法)。這并不會使它成為一個函數聲明,并且函數的名字 和 函數體 也不會被提前到函數作用域的頂部。下面的代碼可以說明我想表達的意思:
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 () {}; // anonymous function expression ('foo' gets hoisted)
function bar() {}; // function declaration ('bar' and the function body get hoisted)
var baz = function spam() {}; // named function expression (only 'baz' gets hoisted)
foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"
How to Code With This Knowledge(在編碼時如何運用這些知識?)
既然你已經了解了作用域和hoisting,那么在JavaScript中對于編寫代碼,它們(作用域和hoisting)意味著什么?最重要的事情是“在聲明你所有的變量時,只使用一個‘var statement’ ”。我強烈建議你在每個作用域內只使用一個var statement,并且把它(var statement)放到作用域頂部。如果你強迫自己這樣做的話,你永遠不會有hoisting相關的困惑。然而,這樣做可能會使得追蹤‘哪些變量是在當前作用域中聲明的’變得困難。我建議在JSLint中設置onevar選項來強制達到這點。如果你按照我要求的去做的話,你的代碼看起來應該像這樣:
/*jslint onevar: true [...] */
function foo(a, b, c) {
? ? var x = 1,
? ? ? ? bar,
? ? ? ? baz = "something";
}
What the Standard Says(那么具體的標準是如何的?)
我發現,想要了解這些‘事情(scoping,hoisting)’是如何運作的 ,直接查閱ECMAScript Standard (pdf)往往是最有幫助的。如下是ECMAScript Standard (pdf)關于變量聲明和作用域的描述:
If the variable statement occurs inside a FunctionDeclaration, the variables are defined with function-local scope in that function, as described in section 10.1.3. Otherwise, they are defined with global scope (that is, they are created as members of the global object, as described in section 10.1.3) using property attributes { DontDelete }. Variables are created when the execution scope is entered. A Block does not define a new execution scope. Only Program and FunctionDeclaration produce a new scope. Variables are initialised to undefined when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.
如果變量聲明出現在函數聲明之內,那么這些變量就被定義在那個函數的函數作用域內,像章節10.1.3中描述那樣。否則,這些變量通過使用property attributes{DontDelete}被定義在全局作用域(即,這些變量被作為global object的成員被創建,像章節10.1.3中描述的那樣)。變量在進入作用域時被創建。一個block不會定義一個新的作用域。只有程序和函數聲明會創建一個新的作用域。變量在創建時被初始化為undefined。帶有初始值的變量在變量聲明被執行時,會被賦予它的賦值表達式的值。而不是變量被創建時。
我希望這篇文章已經揭示了,對JavaScript程序員來說,最困惑的根源之一(scoping,hoisting)。我盡可能的透徹地闡述這件事,并避免在闡述這件事時 制造更多的困惑。如果有什么錯誤或者大的疏忽,請告知我。
本文翻譯自:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html
轉載請注明出處