在撰寫 Javascript 使用 for 迴圈傳遞 function ,i 值不如預期,雖然很快地繞過去了,但還是得回頭補強一下 closure 的知識。
重現問題
當時狀況是這樣的,我的手牌為 J、Q、K、鬼牌,先將動作儲存,以在自己的回合時,直接出牌 J、Q、K !
結果連出三張鬼牌,就沒有晉級世界賭王大賽了。
var funcList = [];
var cards = ['J','Q','K','Joker']
for (var i = 0; i < 3; i++) {
funcList.push(
function () {
console.log(cards[i]);
}
)
}
function 我的回合JQK(){
funcList[0]();
funcList[1]();
funcList[2]();
}
我的回合JQK();
//Joker
//Joker
//Joker
原因解析
上面最直接可觀察到的是 console.log(cards[i]);
中的 i 值被以類似參考的的方式給帶了出去,這現象是閉包(Closure),也就是函式會參考建立當下的 Scope(範疇)。
一般來說 var 變數的 Scope 最小單位是 function,不是 for 區塊,也不是 if 區塊,先看看下面的程式碼
foo();
function foo(){
console.log(i);//undefined
for(var i = 0; i < 3; i++){
//doSomething
}
console.log(i);//3
console.log(j);//ReferenceError: j is not defined
}
console.log(i)//ReferenceError: i is not defined
可以看到當取沒有被宣告過的變數 j 時,Javascript 會說 ReferenceError 因為沒有這個變數,這符合一般理解,而最後一行因為 i 被 function 包起來了,所以找不到這個變數。
但第 3 行的 i 卻是 undefined,這是提升(Hoisting),當一個變數被宣告,它會被拉到所屬 Scope 的上部,所以程式其實是這樣跑的
function foo(){
var i;
console.log(i);//undefined
for(i = 0; i < 3; i++){
//doSomething
}
console.log(i);//3
console.log(j);//ReferenceError: j is not defined
}
foo();
console.log(i)//ReferenceError: i is not defined
i 被提升了,超出 for 迴圈但卻不出 function 的範疇。
回到牌桌上,i 值被參考但不是以傳址的方式,而是它所在的 scope 被參考,這稱作 scope reference。
解決之道
forEach IE 9+
使用 forEach 方法,可以很快地繞過甚至不用懂 Closure 就完成陣列的遍歷,也是遍歷陣列的建議編程方式。
但 IE 8 不支援,可能要注意一下(或拒絕支援 IE 8)
var funcList = [];
var cards = ['J','Q','K','Joker']
cards.forEach(function(card, i){
if(i < 3){
funcList.push(
function(){ console.log(card);}
)
}
});
function 我的回合JQK(){
funcList[0]();
funcList[1]();
funcList[2]();
}
我的回合JQK();
//J
//Q
//K
let, const IE 11 +
ECMAScript 2015 定義,它們的 scope 比較特別,為區塊範疇也就是可以作為 if、for 的局域變數
不過現階段只支援 IE 11 以上,直接只支援 Chrome 好像還比較好說服客戶。
var funcList = [];
var cards = ['J','Q','K','Joker']
for(let i = 0; i < 3; i++){
funcList.push(
function(){ console.log(cards[i]);}
)
}
function 我的回合JQK(){
funcList[0]();
funcList[1]();
funcList[2]();
}
我的回合JQK();
//J
//Q
//K
Scope 原理解
fucntion 每被叫用一次,function 內宣告的變數就會對應對不同的 scope reference ,以此截斷 i 值的糾纏
var funcList = [];
var cards = ['J','Q','K','Joker']
for (var i = 0; i < 3; i++) {
覆蓋一張牌在我的回合發動(i);
function 覆蓋一張牌在我的回合發動(index){
funcList.push(
function () {
console.log(cards[index]);
}
)
}
}
function 我的回合JQK(){
funcList[0]();
funcList[1]();
funcList[2]();
}
我的回合JQK();
//J
//Q
//K
最後
關於 Closure ,不可否認它造成許多人的困擾(不然就不會有 let, const),但用得好也不失為一種優秀特性。