iT邦幫忙

9

Javascript中的傳遞參考與closure (4)

I.傳值與傳參考
II. 傳參考與closure (1)
II. 傳參考與closure (2)
II. 傳參考與closure (3) --> now

這可能是最後一篇文了,大家慢慢欣賞...
現在,來解答一下前一篇文章提出的問題:

for(var i=0; i<10;i++){
  setTimeout(function(){
    console.log(i);  // 10, 10, ...., 10
  }, 500);
}

為什麼這邊印出來的都是 10,如果大家有理解上上一篇跟上一篇提到的一些範例的話,就會知道,在執行 for loop 時,console.log 這行根本沒有被執行到,他是被設定在 500 毫秒後才會執行的東西,真正執行的時候用到了外部變數 i,利用 chrome 你會看到當 timer 的時間到了,開始執行 console.log(i)時,除了 this 之外,根本沒有一個叫做 i 的區域變數:

那 i 在哪裡呢? i 是 global的一個變數,而且因為跑完迴圈了,i 的值是 10。另外各位可以做一個小實驗,把 timer 的時間設定的長一點,例如 5000 (5秒),然後在這個迴圈外面立刻把 i 設成 20,那你就會看 log 出來的是 10 個 20。

由於上一篇,使用到外層變數,該變數是會出現在 Closure 區中,為了維持大家理解的一致性,這邊我們稍為修改一下函式,把它在函式裡頭:

function timer(){
  for(var i=0; i<10;i++){
    setTimeout(function(){
      console.log(i);  // 10, 10, ...., 10
    }, 500);
  }
}

timer();

這邊一樣透過 chrome 來看:

斷點是停在第一次 timeout 時,圖中很清楚可以看到 i 被放到 Closure 這區裡頭,雖然是第一次 timeout,但 i 卻是 10,所以 log 出來的就是 10,這個 10,是當初執行 for loop 時的結果。

但我們真的很想要它log出來的是 0, 1, ..., 9,應該要怎麼做呢? 先想一下造成錯誤狀況的原因:closure儲存的是參考,而不是值。

既然如此,那我們就把值存起來就好了:

function timer(){
  for(var i=0; i<10;i++){
    setTimeout(function(){
      var x = i;
      console.log(x);	 // 10, 10, ...., 10
    }, 500);
  }
}

timer();

我們已經用一個區域變數 x 把 i 給存起來了,為什麼印出來的還是 10, 10,...,10 呢?記得前一篇有解釋過嗎? var x = i;這行,在執行 for-loop 時,根本不會去執行,而是timer時間到了,才會真的去執行,而這時候 i 已經是 10 了。

所以以上的解法也是不對的,正確的解法是要把函式包成 IIFE 的方式來做:
(IIFE:這邊順道帶出這個詞,這種縮寫不用特別理會,只是在 Effective Javascript 中有提到,因為這邊剛好用到了,之後也會用,就順道提一下,IIFE 是 immediately invoked function expression,翻譯成「即刻調用的函式運算式」。)

function timer(){
  for(var i=0; i<10;i++){
    (function(i){setTimeout(function(){
      console.log(i);	 // 0, 1, ...., 9
    }, 500)})(i);
  }
}

timer();

比較一下,這邊 setTimeout 是被包在一個函式之中,再用一組()把這個函式括起來,後面再接一組(),讓這個函式即刻被執行,寫開了其實是這樣:

function work(i){
  setTimeout(function(){
    console.log(i);	 // 0, 1, ...., 9
  }, 500);
}

function timer(){
  for(var i=0; i<10;i++){
    work(i);
  }
}

timer();

為什麼這樣就能成功的印出 10 ? 我們來看一下,在執行 for loop 時,呼叫了 work(i),這時候進入函式 work 裡頭,work 裡頭 i 是 work 的區域變數了,不再是 for loop 裡的那個 i 了,因為 i 只是一個簡單型別的數字,傳遞的僅僅是值,也就是指把 i 的值複製進去,並不是 i 的參考,從此就天涯各兩人,不相干了(這邊可以復習本系列的第一篇)。

觀看 chrome:

這邊我們把斷點停在 setTimeout,也就是剛呼叫 work 進來的第一行程式碼,果然,i 被放在 Local 區域中,setTimeout 後就會結束這個函式的呼叫,回到 for loop。(只是設定 timer,並不會去執行 timer 時間到後要做的事)

等時間到後,開始執行當初 setTimeout 裡頭的那個函式,用 chrome 會看到:

這邊一樣可以看到一個 i 在 Closure 區中,而且這個 i 的值是 0,為什麼呢?因為這個 i 是當初 for loop 時,執行 work(i) 時,製造出來的一個新的區域變數,for loop 執行了十次,work 被呼叫了 10 次,製造了 10 個區域變數,每一個區域變數分別儲存著當初傳進來的值,而又因為被內層函式,setTimeout 裡頭的匿名函式給使用到,所以就存到 closure 去了。

最後整理一下,因為一般不需要拆出 timer 與 worker 這兩個函式,可以直接都用匿名函式與 IIFE 的方式作:

for(var i=0; i<10;i++){
  (function(i){setTimeout(function(){
    console.log(i);	 // 0, 1, ...., 9
  }, 500)})(i);
}
// 或是把傳進去的參數名稱改掉,可避免誤以為是同一個變數,有助於思考理解
for(var i=0; i<10;i++){
  (function(x){setTimeout(function(){
    console.log(x);	 // 0, 1, ...., 9
  }, 500)})(i);
}

最後再提供一個經典案例:

// html 上有 n 個按鈕
var btns = document.getElementsByTagName('button');
for(var j=0, len = btns.length; j<len; j++) {
  btns[j].onclick = function(){
    alert(j);
  };
}

我們期望的是,按下 1 號按鈕,alert出來的是 1、按下 2 號按鈕,alert出來的是 2,但結果卻不是,alert 出來的會是 button 的個數,這是為什麼呢?又該怎麼修改呢?這留給大家自己思考一下。


0
ted99tw
iT邦高手 1 級 ‧ 2013-09-30 08:40:01

azole提到:
這可能是最後一篇文了,大家慢慢欣賞...

離開監獄,不要說再見...毆飛

0
bluechiou358
iT邦新手 5 級 ‧ 2013-10-01 09:38:33

怎麼會是最後一篇!!!

azole iT邦新手 5 級‧ 2013-10-01 10:56:59 檢舉

沒有梗了,不過昨晚有看到一個,今天有時間的話會再寫。謝謝blue~~

0
yhsiang
iT邦新手 5 級 ‧ 2014-01-15 13:41:15

我想了解closure和javascript good part後
有更簡單的做法是
for(k=0; k<10;k++){
setTimeout(function(k){
return function() {
console.log(k);
}
}(k), 500);
}

我要留言

立即登入留言