Javascript for 迴圈中的 Closure

  • 654
  • 0
  • 2017-10-12

在撰寫 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),但用得好也不失為一種優秀特性。

參考資料

你所不知道的 JS