JavaScript 通過函數管理作用域。在函數內部聲明的變量只在這個函數內部,函數外面不可用。另一方面,全局變量就是在任何函數外面聲明的或是未聲明直接簡單使用的。
“未聲明直接簡單使用”,指的是不用 var
關鍵字來聲明變量。這個我們已經非常清楚,避免造成隱式產生全局變量的方法就是聲明變量儘量用 var
關鍵字。
可你以爲用了 var
就 ok 了?來看看這個陷進:
function foo() {
var a = b = 0;
// body…
}
也許你期望得到的是兩個局部變量,但b
卻是貨真價實的全局變量。why?
Because 賦值運算是自右往左的 ,所以這相當於:
function foo() {
var a = (b = 0);
// body…
}
所以 b
是全局變量。
解決:變量聲明,最好一個個來,別搞批發~_~;
變量聲明
先來看陷阱:
myName = “global”;
function foo() {
alert(myName);
var myName = “local”;
alert(myName);
}
foo();
乍看上去,我們預計期望兩次 alert
的結果分別爲 “global” 與 “local”,但真實的結果是 “undefined” 與 “local”。why? Because 變量在同一作用域(同一函數)中,聲明都是被提至作用域頂部先進行解析的。
所以以上代碼片段的執行行爲可能就像這樣:
function foo() {
var myName;
alert(myName); // “undefined”
myName = “local”;
alert(myName); // “local”
}
用另一個陷阱來測試下你是否真的理解了預解析:
if (!(“a” in window)) {
var a = 1;
}
alert(a);
a
變量的聲明被提前到了代碼頂端,此時還未賦值。接下來進入 if
語句,判斷條件中 "a"
in window
已成立(a
已被聲明爲全局變量),所以判斷語句計算結果爲 false
,直接就跳出 if
語句了,所以 a
的值爲 undefined
。
var a; // “undefined”
console.log(“a” in window); // true
if (!(“a” in window)) {
var a = 1; // 不執行
}
alert(a); // “undefined”
解決:變量聲明,最好手動置於作用域頂部,對於無法當下賦值的變量,可採取先聲明後賦值的手法。
函數聲明
函數聲明也是被提前至作用域頂部,先於任何表達式和語句被解析和求值的
alert(typeof foo); // “function”
function foo() {
// body…
}
可以對比一下:
alert(typeof foo); // “undefined”
var foo = function () {
// body…
};
明白了這個道理的你,是否還會掉入以下的陷阱呢?
function test() {
alert(“1″);
}
test();
function test() {
alert(“2″);
}
test();
運行以上代碼片段,看到的兩次彈窗顯示的都是 “2”,爲什麼不是分別爲 “1” 和 “2” 呢?很簡單,test
的聲明先於 test()
被解析,由於後者覆蓋前者,所以兩次執行的結果都是
“2”。
解決:多數情況下,我用函數表達式來代替函數聲明,特別是在一些語句塊中。
函數表達式
先看命名函數表達式,理所當然,就是它得有名字,例如:
var bar = function foo() {
// body…
};
要注意的是:函數名只對其函數內部可見。如以下陷阱:
var bar = function foo() {
foo(); // 正常運行
};
foo(); // 出錯:ReferenceError
解決:儘量少用命名函數表達式(除了一些遞歸以及 debug 的用途),切勿將函數名使用於外部。
函數的自執行
對於函數表達式,可以通過後面加上 ()
自執行,而且可在括號中傳遞參數,而函數聲明不可以。陷阱:
// (1) 這只是一個分組操作符,不是函數調用!
// 所以這裏函數未被執行,依舊是個聲明
function foo(x) {
alert(x);
}(1);
以下代碼片段分別執行都彈窗顯示 “1”,因爲在(1)
之前,都爲函數表達式,所以這裏的 ()
非分組操作符,而爲運算符,表示調用執行。
// 標準的匿名函數表達式
var bar = function foo(x) {
alert(x);
}(1);
// 前面的 () 將 function 聲明轉化爲了表達式
(function foo(x) {
alert(x);
})(1);
// 整個 () 內爲表達式
(function foo(x) {
alert(x);
}(1));
// new 表達式
new function foo(x) {
alert(x);
}(1);
// &&, ||, !, +, -, ~ 等操作符(還有逗號),在函數表達式和函數聲明上消除歧義
// 所以一旦解析器知道其中一個已經是表達式了,其它的也都默認爲表達式了
true && function foo(x) {
alert(x);
}(1);
解決:這個陷阱的關鍵在於,弄清楚形形色色函數表達式的實質。
循環中的閉包
以下演示的是一個常見的陷阱:
var links =document.getElementsByTagName(“ul”)[0].getElementsByTagName(“a”);
for (var i = 0, l = links.length; i < l; i++) {
links[i].onclick = function (e) {
e.preventDefault();
alert(“You click link #” + i);
}
}
我們預期當點擊第 i
個鏈接時,得到此序列索引 i
的值,可實際無論點擊哪個鏈接,得到的都是 i
在循環後的最終結果:”5”。
解釋一下原因:當 alert
被調用時,for
循環內的匿名函數表達式,保持了對外部變量 i
的引用(閉包),此時循環已結束,i
的值被修改爲
“5”。
解決:爲了得到想要的結果,需要在每次循環中創建變量 i
的拷貝。以下演示正確的做法:
var links =document.getElementsByTagName(“ul”)[0].getElementsByTagName(“a”);
for (var i = 0, l = links.length; i < l; i++) {
links[i].onclick = (function (index) {
return function (e) {
e.preventDefault();
alert(“You click link #” + index);
}
})(i);
}
可以看到,(function
() { ... })()
的形式,就是上文提到的 函數的自執行 ,i
作爲參數傳給了 index
,alert
再次執行時,它就擁有了對 index
的引用,此時這個值是不會被循環改變的。當然,明白了其原理後,你也可以這樣寫:
for (var i = 0, l = links.length; i < l; i++) {
(function (index) {
links[i].onclick = function (e) {
e.preventDefault();
alert(“You click link #” + index);
}
})(i);
}