javascript 的作用域
翻譯自:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html
如果以下代碼執行,你知道出什麽結果嗎?
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的範圍。
javascript的作用域
JavaScript初學者最為混亂的原因之一就是範圍界定。 其實這不只是初學者。 我遇到了很多經驗豐富的JavaScript程序員,他們不完全了解範圍界定。 在JavaScript中如此混亂的原因是因為它看起來像一個C系列語言。 思考以下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。這是因為C和C系列的其余部分具有塊級範圍。 當控件進入塊(如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具有功能級的作用域。 這與C系列截然不同。 塊(如if語句)不會創建新的範圍。 只有函數創建一個新的範圍。
對於習慣於C,C ++,C#或Java等語言的程序員來說,這是意想不到的,不受歡迎的。 幸運的是,由於JavaScript功能的靈活性,有一個解決方法。 如果必須在函數內創建臨時作用域,請執行以下操作:
function foo() { var x = 1; if (x) { (function () { var x = 2; // some other code }()); } // x is still 1. }
這種方法實際上是非常靈活的,可以在任何需要臨時作用域的地方使用,而不僅僅是在塊語句中。 但是,我強烈建議您花點時間真正了解並欣賞JavaScript範圍。 這是非常強大的,是我最喜歡的語言功能之一。 如果你明白範圍界定,hosting將會更有意義。
hosting
在JavaScript中,一個名稱以四種基本方式之一輸入範圍:
語言定義:默認情況下,所有作用域都給出了這個和參數的名稱。
形式參數:函數可以具有命名形式參數,這些參數範圍是該函數的正文。
函數聲明:這些是函數foo(){}的形式。
變量聲明:這些形式為var foo ;
函數聲明和變量聲明始終被JavaScript解釋器移動(“hosting”)到其包含範圍的頂部。 功能參數和語言定義的名稱顯然已經在那裏。 這意味著這樣的代碼:
function foo() { bar(); var x = 1; }
事實上是這樣:
function foo() { var x; bar(); x = 1; }
事實證明,包含聲明的行是否將被執行並不重要。 以下兩個功能是等效的:
function foo() { if (false) { var x = 1; } return; var y = 1; } function foo() { var x, y; if (false) { x = 1; } return; y = 1; }
請註意,聲明的轉讓部分未被吊起。 只有這個名字已經懸掛。 函數聲明不是這樣,整個函數體也將被掛起。 但請記住,有兩種正常的方法來聲明函數。 考慮以下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!"); } function bar() { // function declaration, given the name ‘bar‘ alert("this will run!"); } } test();
在這種情況下,只有函數聲明的主體已經懸掛在頂部。 “foo”的名字已經懸掛,但身體被遺棄,在執行過程中被分配。
這涵蓋了起重機的基礎知識,並不像看起來那麽復雜或混亂。 當然,這是JavaScript,在某些特殊情況下還有一點復雜性。
名稱解析順序
要記住的最重要的特殊情況是名稱解析順序。請記住,名稱輸入給定範圍有四種方式。我上面列出的順序是它們解決的順序。一般來說,如果一個名稱已經被定義,它不會被同名的另一個屬性所覆蓋。這意味著函數聲明優先於變量聲明。這並不意味著對該名稱的分配將不起作用,只是聲明部分將被忽略。還有一些例外:
內置的名稱參數的行為奇怪。它似乎是在形式參數之後聲明的,但在函數聲明之前。這意味著具有name參數的形式參數將優先於內置,即使未定義。這是一個不好的功能。不要使用name參數作為形式參數。
嘗試使用名稱作為任何位置的標識符將導致一個SyntaxError。這是一個很好的功能。
如果多個形式參數具有相同的名稱,則列表中最新出現的一個參數將優先,即使未定義。
命名函數表達式
您可以給函數表達式中定義的函數命名,其語法類似函數聲明。 這不是一個函數聲明,並且這個名字沒有被引入到範圍內,而且這個名字也沒有被提起來。 這裏有一些代碼來說明我的意思:
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"
如何在代碼中應用
現在您已經了解了範圍和提升情況,這對JavaScript編程意味著什麽? 最重要的是始終使用var語句聲明變量。
我強烈建議您每個範圍只有一個var語句,它位於頂部。 如果你強迫自己這樣做,你永遠不會有起重的混亂。
但是,這樣做可能難以跟蹤在當前範圍內實際聲明了哪些變量。 我建議使用JSLint與onevar選項來強制執行。
如果你完成了所有這些,你的代碼應該是這樣的:
/*jslint onevar: true [...] */
function foo(a, b, c) {
var x = 1,
bar,
baz = "something";
}
標準是什麽呢?
我發現直接咨詢ECMAScript標準(pdf)通常是有用的,以了解這些事情如何工作。 以下是對變量聲明和範圍的說明(舊版本中的第12.2.2節):
如果變量語句發生在FunctionDeclaration內,則在該函數中使用函數局部作用域定義變量,如第10.1.3節所述。 否則,使用屬性屬性{DontDelete}定義全局範圍(即,它們被創建為全局對象的成員,如第10.1.3節所述)。 在輸入執行範圍時創建變量。 塊不定義新的執行範圍。 只有程序和函數聲明才能產生一個新的範圍。 變量在創建時被初始化為未定義。 當執行VariableStatement時,具有Initialiser的變量將分配給其AssignmentExpression的值,而不是創建變量時。
我希望這篇文章揭示了JavaScript程序員最常見的困惑之一。 我試圖盡可能的徹底,避免造成更多的混亂。 如果我有任何錯誤或遺漏,請通知我。
javascript 的作用域