原文連接html
你知道下面這段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);
複製代碼
瀏覽器會alert("1")。那麼,到底發生了什麼?這看起來可能很陌生、很古怪而且使人困惑,但這正是這個語言強而有力的表現特徵。我不清楚這個特徵的專有名詞,可是我更願意用「hoisting」來表達。這篇文章將試着去揭開這種機制的面紗,可是咱們先着重理解JavaScript的做用域。express
對於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系的語言都有塊級做用域(block-level scope)當流程控制走進了塊級域,例以下面的if語句塊,能夠在這個做用域聲明一個新的變量,而不影響外部的做用域。這不一樣於JavaScript。在Firebug裏面試試下面的代碼:bash
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 scope)。這徹底不一樣與C系語言的塊級做用域,例以下面的if語句塊裏面不會建立一個新的做用域。只有function才能建立新的做用域。ide
對於許多熟悉像C,C++,C#或者Java的程序員來,這個設定是超出預期而且不友好的。幸運的是,鑑於JavaScript中函數的靈活性,有個曲線救國的方法。若是你必定要在function中建立臨時的做用域,能夠作以下嘗試:函數
function foo() {
var x = 1;
if (x) {
(function () {
var x = 2;
// some other code
}());
}
// x is still 1.
}
複製代碼
這個方法確實很靈活,而且能在任什麼時候候須要臨時做用域時使用,不只僅侷限在塊語句中。可是,我強烈的建議你真的須要應該花些時間去理解和正確的認識JavaScript的做用域。它真的頗有用,也是這個語言吸引個人特點之一。若是你理解了做用域,hoisting對你來講也將變得容易許多。ui
在JavaScript,以一個名稱存在於做用域有如下4中方法:this
1.語言自己定義(Language-defined):全部做用域默認包含this和arguments。
2.形式參數(Formal parameters):函數能帶入形式參數,使其能從函數外部做用域進入函數內部做用域。
3.函數聲明(Function declarations):這是函數聲明的形式 function foo(){}。
4.變量聲明(Variable declarations):聲明的形式 var foo;
函數的聲明和變量的聲明老是被JavaScript編譯器偷偷的提高(「hoisted」)到它們所在做用域的頂部。函數參數和語言自己定義的已經明顯的存在在那裏。這種形式像下面這段代碼:
function foo() {
bar();
var x = 1;
}
複製代碼
事實上被編譯成下面這樣:
function foo() {
var x;
bar();
x = 1;
}
複製代碼
結論是聲明的那行是否被執行都是可有可無的。下面兩個function是等價的:
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」被提高了,可是它的函數體部分被遺留在賦值的時候執行。
以上涵蓋了基本的提高(「hoisting」),並非看起來那麼複雜和使人迷惑。固然,做爲JavaScript,在特殊狀況下是會有那麼一些複雜的東西。
在大多數重要特殊的時候應時刻銘記在心是名稱解析的順序。牢記一個名稱進入做用域有四種方法。我在上面列舉的例子就是他們解析的順序。總的來講,若是一個名稱已經被定義,它永遠不會被另外一個同名的不一樣屬性覆蓋。這意味着函數的聲明要優先於變量的聲明。但這並不表明對着名稱的複製不起做用,僅僅只是聲明部分被忽略。這裏有一些例外:
你能夠在函數表達式中給函數定義名稱,就像函數聲明的語句同樣。這樣並不能使它成爲函數的聲明,而且這個名稱沒有被帶入到做用域,函數體也沒有被提高。下面是一些代碼來闡明個人意思:
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 Standard(PDF)理解這些東西是如何運做的方式是最有用的。這是我討論的關於變量神經和做用域的段落(section 12.2.2 in the older version):
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.
我但願這篇文章能幫助到許多那些有着共同困惑的JavaScript程序員。我已經很努力盡量的直接的闡述,避免製造更多的困惑。若是我寫錯了或者遺漏了什麼,請讓我知道。