函數軍隊
以下程式碼會建立一個 shooters
陣列。
每個函數都應該輸出其數字。但有些地方出錯了…
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let shooter = function() { // create a shooter function,
alert( i ); // that should show its number
};
shooters.push(shooter); // and add it to the array
i++;
}
// ...and return the array of shooters
return shooters;
}
let army = makeArmy();
// all shooters show 10 instead of their numbers 0, 1, 2, 3...
army[0](); // 10 from the shooter number 0
army[1](); // 10 from the shooter number 1
army[2](); // 10 ...and so on.
為什麼所有射手都顯示相同的數值?
修正程式碼,讓它們能按預期運作。
讓我們檢視一下 makeArmy
內部實際發生了什麼,解決方案就會變得顯而易見。
-
它建立一個空的陣列
shooters
let shooters = [];
-
透過迴圈中的
shooters.push(function)
填入函式。每個元素都是一個函式,因此產生的陣列看起來像這樣
shooters = [ function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); } ];
-
陣列從函式中傳回。
然後,稍後,呼叫任何成員,例如
army[5]()
會從陣列中取得元素army[5]
(一個函式)並呼叫它。現在,為什麼所有這些函式都顯示相同的值
10
?那是因為在
shooter
函式內部沒有區域變數i
。當呼叫此類函式時,它會從其外部詞法環境中取得i
。那麼,
i
的值會是什麼?如果我們檢視原始碼
function makeArmy() { ... let i = 0; while (i < 10) { let shooter = function() { // shooter function alert( i ); // should show its number }; shooters.push(shooter); // add function to the array i++; } ... }
我們可以看到所有
shooter
函式都是在makeArmy()
函式的詞法環境中建立的。但是當呼叫army[5]()
時,makeArmy
已完成其工作,而i
的最後值為10
(while
停止於i=10
)。因此,所有
shooter
函式從外部詞法環境取得相同的值,也就是最後一個值i=10
。如上所示,在
while {...}
區塊的每個反覆運算中,都會建立一個新的詞法環境。因此,為了修正這個問題,我們可以將i
的值複製到while {...}
區塊中的變數,如下所示function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let j = i; let shooter = function() { // shooter function alert( j ); // should show its number }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); // Now the code works correctly army[0](); // 0 army[5](); // 5
這裡的
let j = i
宣告一個「反覆運算區域」變數j
並將i
複製到其中。基本型別會「依值」複製,因此我們實際上會取得i
的一個獨立副本,屬於目前的迴圈反覆運算。射手可以正確運作,因為
i
的值現在離得更近一點。不在makeArmy()
詞法環境中,而是在對應於目前迴圈反覆運算的詞法環境中如果我們一開始使用
for
,也可以避免此類問題,如下所示function makeArmy() { let shooters = []; for(let i = 0; i < 10; i++) { let shooter = function() { // shooter function alert( i ); // should show its number }; shooters.push(shooter); } return shooters; } let army = makeArmy(); army[0](); // 0 army[5](); // 5
這基本上是相同的,因為每個反覆運算中的
for
會產生一個新的詞法環境,並有其自己的變數i
。因此,每個反覆運算中產生的shooter
會參考它自己的i
,從那個反覆運算中。
現在,由於你已經投入了這麼多心力閱讀這篇文章,而最終的食譜如此簡單 - 只需要使用 for
,你可能會想 - 這樣值得嗎?
嗯,如果你可以輕易回答這個問題,你就不會閱讀這個解答。因此,希望這個任務一定能幫助你更了解事情。
此外,確實有時候人們會偏好 while
而非 for
,以及其他情境,其中此類問題是真實存在的。