本文內容
- 閉包
- 閉包和引用
- 參考資料
閉包是 JavaScript 的重要特性,非常強大,可用於執行復雜的計算,可並不容易理解,尤其是對之前從事物件導向程式設計的人來說,對 JavaScript 認識和程式設計顯得更難。特別是在看一些開源的 JavaScript 程式碼時感覺尤其如此,跟看天書沒什麼區別。
一般情況下,人們認識一個事物,會根據之前的經驗,進行對比和總結,在腦中建立一個模型,從而理解掌握它,但是 JavaScript 與物件導向程式設計實在“沒有可比性”,最明顯的是某過於寫法,總覺得“怪怪的”,更不用說,其一些高階特性。如果說“物件”在物件導向程式設計時的出現相當有規律,但是在 JavaScript 中則毫無規律,無處不在,甚至在你意想不到的地方。
首先看兩段程式碼。
示例 1:
示例 2:
示例 1 和示例 2 都是閉包,只是 2 比 1 複雜,甚至還有更復雜的寫法,比如返回多個閉包。
示例 1,指令碼被載入記憶體後,並沒有為函式 sayHelloWorld() 計算變數 sMessage 的值。該函式捕獲 sMessage 的值只是為了以後的使用,也就是說,解釋程式知道在呼叫該函式時要檢查 sMessage 的值。sMessage 將在函式呼叫 sayHelloWorld() 時(最後一行)被賦值,顯示訊息 "hello world"。
示例 2,函式 addNum() 包括函式 doAdd() (閉包)。內部函式是一個閉包,因為它將獲取外部函式的引數 iNum1 和 iNum2 以及全域性變數 iBaseNum 的值。 addNum() 的最後一步呼叫了 doAdd(),把兩個引數和全域性變數相加,並返回它們的和。
這裡要掌握的重要概念是,doAdd() 函式根本不接受引數,它使用的值是從執行環境中獲取的。
閉包
閉包,根據 ECMAScript 描述,詞法(lexically)表示包括不被計算的變數的函式,函式可以使用函式之外定義的變數,它意味著當前作用域總能夠訪問外部作用域中的變數。函式是 JavaScript 中唯一擁有自身作用域的結構,因此閉包的建立依賴於函式。函式內部的函式訪問其所在函式的變數(區域性變數、形參),這些變數會受到內部函式的影響,當其外部函式外被呼叫時,就會形成閉包。內部的函式會在其外部函式返回後,被執行。
示例 3:
function foo() {
var a = 10;
function bar() {
a *= 2;
return a;
}
return bar; // 返回內部函式 bar
}
var baz = foo();
baz(); // 20
說明:
- foo 是 bar 的外部函式,bar 是 foo 的內部函式;a 是 foo 的區域性變數;
- bar 訪問 foo 的區域性變數 a;
- foo 返回 bar。
- bar 在 foo 的外部被呼叫。
當執行 baz() 後,閉包使 Javascript 垃圾回收機制不會回收 foo 所佔的資源。因為,baz 實際指向 foo 的內部函式 bar,bar 依賴 foo 的區域性變數 a。這樣,在執行 var baz=foo() 後,baz 實際指向了 bar,而不是 foo。bar 訪問了 foo 的區域性變數 a,當執行 baz() 後,a 為 20。這就形成了一個閉包。如下圖所示:
圖 1
如果把 foo 看作是一個包,根據剪頭指示,形成了一個閉包。結果是區域性變數 a 的永續性(如示例 4 所示)。下面程式碼就不是閉包。無論執行多少次,都是顯示 20。
示例 4:
function foo() {
var a = 10;
function bar() {
alert(a *= 2);
}
bar();
}
foo(); // 20
foo(); // 20
foo(); // 20
從以上兩個示例看,閉包有點類似於物件導向的介面和委託,——只是呼叫方法而無需知道具體細節。
示例 5:
function foo() {
var a = 10;
function bar() {
a *= 2;
return a;
}
return bar;
}
var baz = foo();
baz(); // 20
baz(); // 40
baz(); // 80
var blat = foo();
blat(); // 20
閉包和引用
模擬私有變數
程式碼 6:
function Counter(start) {
var count = start;
return {
increment: function () {
count++;
},
get: function () {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
這裡,Counter 函式返回兩個閉包,函式 increment 和函式 get。 這兩個函式都維持著 對外部作用域 Counter 的引用,因此總可以訪問此作用域內定義的變數 count.
為什麼不能在外部訪問私有變數
因為 JavaScript 中不可以對作用域進行引用或賦值,因此沒有辦法在外部訪問 count 變數,唯一的途徑就是通過上面那兩個閉包。
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
迴圈中的閉包
一個常見的錯誤出現在迴圈中使用閉包,假設我們需要在每次迴圈中呼叫迴圈序號,
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
上面的程式碼不會輸出數字 0 到 9,而是會輸出數字 10 十次。
當 console.log 被呼叫的時候,匿名函式保持對外部變數 i 的引用,此時for迴圈已經結束, i 的值被修改成了 10.
為了得到想要的結果,需要在每次迴圈中建立變數 i 的拷貝。
避免引用錯誤
為了正確的獲得迴圈序號,最好使用 匿名包裹器(自執行匿名函式)。
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
外部的匿名函式會立即執行,並把 i 作為它的引數,此時函式內 e 變數就擁有了 i 的一個拷貝。
當傳遞給 setTimeout 的匿名函式執行時,它就擁有了對 e 的引用,而這個值是不會被迴圈改變的。
有另一個方法完成同樣的工作;那就是從匿名包裝器中返回一個函式。這和上面的程式碼效果一樣。
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
參考資料
- w3c 閉包
- WIKI 閉包
- JavaScript Garden Closure
- 閉包的概念、形式與應用 (IBM DeveloperWorks)
- 跨越邊界: 閉包 (IBM DeveloperWorks)