(轉自:http://bbs.tianya.cn/post-itinfo-280080-1.shtml)
Node.js 的異步機制由事件和回調函數實現,一開始接觸可能會感覺違反常規,但習慣 以后就會發現還是很簡單的。然而這之中其實暗藏了不少陷阱,一個很容易遇到的問題就是 循環中的回調函數,初學者經常容易陷入這個圈套。讓我們從一個例子開始說明這個問題。
-
var fs = require('fs');
-
var files = ['a.txt', 'b.txt', 'c.txt'];
-
-
for (var i = 0; i < files.length; i++) {
-
fs.readFile(files[i], 'utf-8', function (err, contents) {
-
console.log(files[i] + ': ' + contents);
-
-
});
-
}
這段代碼的功能很直觀,就是依次讀取文件 a.txt、b.txt 、c.txt ,並輸出文件名和內容。假設這三個文件的內容分別是 AAA 、BBB 和 CCC,那么我們期望的輸出結果就是:
a.txt: AAA
b.txt: BBB
c.txt: CCC
可是我們運行這段代碼的結果是怎樣的呢?竟然是這樣的結果:
undefined: AAA
undefined: BBB
undefined: CCC
這個結果說明文件內容正確輸出了,而文件名卻不對,也就意味着,contents 的結果是正確的,但 files[i] 的值是 undefined。這怎么可能呢,文件名不正確卻能讀取文件內容?既然難以直觀地理解,我們就把 files[i] 分解並打印出來看看,在讀取文件的回調函數中分別輸出 files、i 和 files[i] 。
-
var fs = require('fs');
-
var files = ['a.txt', 'b.txt', 'c.txt'];
-
for (var i = 0; i < files.length; i++) {
-
fs.readFile(files[i], 'utf-8', function (err, contents) {
-
console.log(files);
-
console.log(i);
-
console.log(files[i]);
-
});
-
}
運行修改后的代碼,結果如下:
[ 'a.txt', 'b.txt', 'c.txt' ]
3
undefined
[ 'a.txt', 'b.txt', 'c.txt' ]
3
undefined
[ 'a.txt', 'b.txt', 'c.txt' ]
3
undefined
看到這里是不是有點啟發了呢?三次輸出的 i 的值都是 3 ,超出了 files 數組的下標范圍,因此 files[i] 的值就是 undefined 了。這種情況通常會在 for 循環結束時發生,例如 for (var i = 0; i < files.length; i++),退出循環時 i 的值就files.length 的值。既然 i 的值是 3 ,那么說明了事實上 fs.readFile 的回調函數中訪問到的 i 值都是循環退出以后的,因此不能分辨。而 files[i] 作為 fs.readFile 的第一個參數在循環中就傳遞了,所以文件可以被定位到,而且可以顯示出文件的內容。
現在問題就明朗了:原因是3 次讀取文件的回調函數事實上是同一個實例,其中引用到的 i 值是上面循環執行結束后的值,因此不能分辨。如何解決這個問題呢?我們可以利用
JavaScript 函數式編程的特性,手動建立一個閉包:
//forloopclosure.js
-
var fs = require('fs');
-
-
var files = ['a.txt', 'b.txt', 'c.txt'];
-
-
for (var i = 0; i < files.length; i++) {
-
(function (i) {
-
fs.readFile(files[i], 'utf-8', function (err, contents) {
-
console.log(files[i] + ': ' + contents);
-
});
-
})(i);
-
}
上面代碼在 for 循環體中建立了一個匿名函數,將循環迭代變量 i 作為函數的參數傳遞並調用。由於運行時閉包的存在,該匿名函數中定義的變量(包括參數表)在它內部的函數(fs.readFile 的回調函數)執行完畢之前都不會釋放,因此我們在其中訪問到的 i 就分別是不同的閉包實例,這個實例是在循環體執行的過程中創建的,保留了不同的值。
補充:閉包的寫法,無法保證按數組存放文件順序讀取文件內容,相當多個文件讀取操作並行進行,根據文件大小決定讀取的快慢;而forEach是可以的保證順序讀取;
事實上以上這種寫法並不常見,因為它降低了程序的可讀性,故不推薦使用。大多數情況下我們可以用數組的 forEach 方法解決這個問題:
//callbackforeach.js
-
var fs = require('fs');
-
var files = ['a.txt', 'b.txt', 'c.txt'];
-
files.forEach(function (filename) {
-
fs.readFile(filename, 'utf-8', function (err, contents) {
-
console.log(filename + ': ' + contents);
-
});
-
});