這篇中,我們要加深大家對 Closure 的觀念。
在上一篇,我們提到要觀察到 Closure 有幾個條件:
藉由這樣,我們才能在外部的 Scope,查找被包裹的 Function 的外層 Scope 環境。
接著我們要證明,其實 Closure 在 JavaScript 中隨處可見。
基本上只要傳遞了 Funciton,十之八九就有 Closure 產生,就讓我們用大家常常使用的 setTimeout 來當作範例:
function asyncLog(message) {
  setTimeout(function() {
    console.log(message);			// "logged after one second"
  }, 1000);
}
asyncLog('logged after one second');
相信大家都很清楚以上的範例程式會有甚麼行為。當我們執行 asyncLog 之後,他會在一秒後印出 "logged after one second"。看似理所當然,但有發現這裡面有 Closure 存在嗎?
讓我們看看 setTimeout,setTimeout 能吃兩個參數:一個是 callback,也就是延遲後要做的 Function、一個是delay,也就是延遲時間,因此我們可以把 setTimeout 簡單看成是這樣:
function setTimeout(callback, delay)  {
  // wait for delay
  // ...
  callback();
}
就是 callback 在等待了一段時間後被執行。
再回頭看一下 asyncLog 的 setTimeout,他傳入了一個匿名函式,這個匿名函式中要做的事很簡單,就是印出 message。那問題來了,一秒後 asyncLog 早就已經被執行完了,理論上asyncLog 的 Scope 早就已經被消滅,為什麼還能印出 message 呢?
沒錯,就是透過 Closure。setTimeout 中的 匿名函式的 Scope,在被指派給 callback 這個參數,在等待了一秒後 callback() 執行,匿名函式的 Scope 此時被建立出來了,為了尋找 message ,JavaScript 從 匿名函式的 Scope 往上找到 ayncLog 的參數 message,而裡面的值就是我們的答案 "logged after one second"!
讓我們對照一下觀察到 Closure 的條件:
有巢狀 Scope 的存在?
有,就是 setTimeout 中的匿名函式。
傳遞了被包裹的 Function?
有,匿名函式被指派給 setTimeout 中的參數 callback。
被傳遞的 Function 在原有的 Scope 以外被執行?
有,因為匿名函式被傳遞給參數 callback 後就在 setTimeout 的 Scope 中被執行了,而不是在原來 asyncLog 的 Scope 中執行。
這個範例符合了我們講的觀察到 Closure 的條件,而正是因為 匿名函式被傳遞到了外部,因此讓我們可以觀察到 Closure。
請記住,Closure 是隨處可見的,只要有 Function 被傳遞的行為發生,十之八九就可以觀察到 Closure。
Closure 非常常見且非常實用的,為了讓大家有更正確的觀念,接下來,我們會再用一個範例來釐清 Closure。
setTimeout 與 for今天我們想要在延遲一秒後印出 0 ~ 4,讓我們來看看這個範例能不能達到我們的要求:
for(var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);				// 5 5 5 5 5
  }, 1000);
}
這個範例的答案其實是在一秒後印出五個 5,感到驚訝嗎?
其實原因很簡單,在這個範例中,setTimeout 隨著 for 被執行了五次,此時 i 已經是 5 了,等待一秒之後,傳入 setTimeout 的五個匿名函式都被執行,並且往外查找 i。理所當然的這五個匿名函式都會查找到同一個 i,這個 i 的內容是 5,所以最後的答案自然就會是 5 5 5 5 5。
那要如何印出預期中的 0 1 2 3 4 呢?只要我們能夠讓匿名函式往上查找到一個變數,而這個變數可以保持當下 i 值的變數就好,那我們就成功了。
for(var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);			// 0 1 2 3 4
  	}, 1000);
  })(i);
}
這個解法中,每次迭代中,我們都會建立出一個新的 IIFE,並且將當下迭代的 i 值當作參數傳入 IIFE 中,換句話說就是,這些 IIFE 的 Scope 中記錄著當下迭代的 i 值。當匿名函式被 setTimeout 執行而往外層查找 i 時,就會拿到每次迭代的 i 值了,因此我們就可以印出 0 1 2 3 4。
在本篇,我們用範例來釐清 Closure 的觀念。
Closure 是隨處可見的,基本上只要傳遞了 Funciton,十之八九就有 Closure 產生,例如 setTimeout 的第一個參數是要傳入 Function,等待一秒後在執行,這就是一個很好的 Closure 例子。
You Don't Know JS: Scope & Closure