閉包是JavaScript比較有意思的特性,也是比較難搞懂的一個概念。
典型示例
一個比較典型的例子就是列印循環計數——
首先我們寫一個小循環,直接列印循環變量
i
function testA() {
for(var i = ; i < ; i++) {
console.log("current: " + i);
}
};
這個程式的輸出很簡單
current: 0
current: 1
current: 2
current: 3
current: 4
current: 5
current: 6
current: 7
current: 8
current: 9
接下來做一點小改變——不再循環中立即列印變量了,而是延遲一段時間再列印(類似的是在循環中給div标簽添加onClick監聽,等到使用者點選時再輸出變量值),這時候代碼變為:
function testB() {
for(var i = ; i < ; i++) {
setTimeout(function() {
console.log("current: " + i);
}, );
}
}
運作這個方法,輸出:
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
顯然沒有符合預期,所有延遲的調用都輸出了循環變量
i
最後的值。如果機械的記憶書本上的概念就是閉包隻能取得包含函數中任何變量的最後一個值。
閉包的使用
換一種思路其實不難了解。
這就好比一個勞工生産一批零件,每生産和一個,他就應當在這個零件上列印一個序号。然而這個勞工忘記了這道程式,又很不巧列印序号的機器很智能,每監測到生産一個零件,序号就自動加1。當這個勞工完成工作以後,他忽然想起忘記列印序号的工序了,他拿起機器就往零件上列印,結果發現所有序号都一樣……
這就十分悲劇了。要想列印上正确的序号,必須記住要每生産一個零件以後就列印,萬萬不可等完工後再來這道工序。
類似的,在
setTimeout
中,我們傳遞了一個回調函數延遲調用,回調函數就好比列印序号這道工序,當時沒有執行,等到執行時讀取的都是變量
i
,自然拿到的就是最後的值了。
是以就需要循環中每調用一次
setTimeout
就儲存住目前的循環變量
i
。那這如何實作呢?
下面這段程式可以給我們一些啟示:
var num = ;
function testNum(_num){
_num = ;
}
testNum(num);
console.log(num); // 5
由于函數參數是按值傳遞的,傳遞給
testNum
的隻是num的值,在函數裡如何改變型參的值,是不影響原變量的。
這個特性就非常好了,既然我們想儲存循環變量的每一個值,那就每循環一步,調用一個函數,把循環變量傳進去就好了。這樣我們在函數的内部,永遠拿到的是調用這個函數時型參對應原變量的值。
function testB() {
for (var i = ; i < ; i++) {
help(i);
}
}
然後在這個方法裡再去設定延時任務
function help(num) {
setTimeout(function () {
console.log("current: " + num);
}, );
}
function testB() {
for (var i = ; i < ; i++) {
help(i);
}
}
這樣程式輸出就是1~10了。
進一步優化一下,僅為了儲存循環變量,就在外面聲明一個函數,非常浪費。在Javascript中更好的做法是聲明一個匿名函數,并立即調用它
function testB() {
for (var i = ; i < ; i++) {
// 匿名函數包含一個參數num
(function(num) {
setTimeout(function () {
console.log("current: " + num);
}, );
})(i); // 立即調用了匿名函數,確定i目前值儲存在閉包的環境中
}
}
這種寫法不太直覺,因為我們無法直接看出循環體中做了什麼——真正關鍵的
setTimeout
是在匿名函數中調用的,多嵌套了一層。如果在循環中直接調用
setTimeout
意圖就更加清晰了。按着這個思路,先将不使用匿名函數的版本做一下調整:
function help(num) {
return function() {
console.log("current: " + num);
}
}
function testC() {
for (var i = ; i < ; i++) {
setTimeout(help(i), ); // 立即執行help儲存i的值
}
}
由于
setTimeout
第一個參數回調是函數類型,是以我們需要在
help
方法中傳回一個函數,這樣調用
help
後,将傳回的匿名函數傳遞給setTimeout。
同樣的,在這裡聲明一個單獨的函數有些浪費,再次改為匿名函數的版本:
function testC() {
for (var i = ; i < ; i++) {
setTimeout((function (num) {
return function () {
console.log("current: " + num);
}
})(i), );
}
}
傳
setTimeout
第一個參數時候,定義了一個傳回匿名函數的匿名函數,并立即執行它,達到了同樣的效果。
應用案例
最後看一個實際應用的例子。
在node.js中可以使用
fs.readdir
函數來周遊給定檔案夾,傳回的結果是檔案夾中除
.
,
..
以外所有檔案/檔案夾的數組
files
。
var filePath = path.join(__dirname, "someFolderName");
fs.readdir(filePath, function (err, files) {
parseResult(err, files);
});
繼而在
parseResult
中,我們想對這個數組進行處理,篩選出其中的檔案夾進行進一步的操作
function parseResult(err, files) {
var length = ;
for (var tmp in files) {
length++;
}
for (var item in files) {
var filePath = path.join(__dirname, "someFoladerName/" + files[item]);
fs.stat(filePath, (function (num) {
console.log("the num is " + num);
var currentItem = num;
return function (err, stats) {
if (stats.isDirectory) {
// 檢測到檔案夾,做相應處理
}
if (currentItem == (length - )) { // currentItem是string類型的序号,轉換後判斷
// 對所有檔案/檔案夾進行的stat異步調用完成後,進行最後操作
}
}
})(item)); // 立即調用匿名函數,儲存item目前值并傳回其中聲明的處理函數
}
}