什麼是Closure閉包呢?


閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。
from MDN



lexical的意思
代表著區塊間的包裹關係,被包裹在內層的區塊可以保護自己的變數不被外層取用,相反的外層區塊的變數還是可以被內層區塊使用



作用域的意思
指的正是作用域環境在程式碼指定變數時,使用 location 來決定該變數用在哪裡的事情。巢狀函式的內部函式,能訪問在該函式作用域之外的變數。


這樣有可能還是不太懂,我們直接舉個例子好了,下面這個函式有幾個特色
(1) init() 建立了局部變數 name 與 displayName() 函式
(2) displayName() 是個在 init() 內定義的內部函式,且只在該函式內做動。displayName() 自己並沒有局部變數
(3) 由於displayName自己沒有局部變數,他就會向外找,找到var name這個變數

function init() {
  var name = "Mozilla";         // name 是個由 init 建立的局部變數
  function displayName() {      // displayName() 是內部函式,一個閉包
    alert(name);                // 使用了父函式宣告的變數
  }
  displayName();
}
init();

一般閉包

題目 - 做出一個加法函式

var addSix = createBase(6);
addSix(10); // returns 16
addSix(21); // returns 27

解題

createBase(baseNumber) {
    return function(x) {
        return x + baseNumber
    }
}

var addSix = createBase(6)
addSix(10); // returns 16
addSix(21); // returns 27

題目詳解

(1) 這題我們定義一個帶有單一參數baseNumber,並且回傳一個新函式的createBase()函式
(2) 本質上,createBase()是一個韓式工廠,他建立一個基本值 (3) addSix兩個都是閉包,他們共享了createBase的定義,但是保有不同的環境(兩個分別傳了不同的參數進去)

迴圈中建立閉包

每隔一秒印出加一的函示

假設今天我想做一個印出 1 -> 2 -> 3 (每隔1s,印出),因此用 for迴圈 + setTimeout 做出了下面的code

for(var i = 0; i < 3; i++) {
    setTimeout(()=>{
        console.log(i)        // 3 -> 3 -> 3 (每隔一秒印出一個3)
    }, 1000 * i)
}

不過結果發現,每隔一秒會印出一個數字是沒錯,但是全部都印出 3,是為什麼??


這邊要另外提到 var、let,在for迴圈設置變數的差異

var - function scope

用var設定一個變數,他離開for迴圈後還是存在,就像以下

for(var good = 1; good < 5; good++) {
}

console.log(good);    # 5

let - block scope

用let設定一個變數,他離開for迴圈後,會錯誤

for(let good = 1; good < 5; good++) {
    
}
console.log(good);  // ReferenceError: good is not defined

簡單說就是,var離開迴圈後還會繼續作用,而let不行,所以var的for迴圈才印得出來
變數提升是什麼?


看完上面 var vs. let 跑迴圈,這樣就可以說明,為什麼一開始使用var設定變數setTimeout的for迴圈, 會全部印出3了,因為var離開迴圈,它還是存在全域變數裡面,所以setTimeout從webAPI轉回來時,他接到的i就是3

解決方法

這就會牽扯到 Closure閉包 的概念

直接舉個例子比較好懂,一樣用剛剛的for回圈來講解

> 把var i = 0 改成let i = 0
> 這樣寫就可以做到我們想要的效果了 0 -> 1 -> 2

> for(let i = 0; i < 3; i++) {
>     setTimeout(()=>{
>         console.log(i)       # 0 -> 1 -> 2
>     }, 1000 * i)
> }

原因就是因為閉包的關係,當setTimeout執行時,如果for迴圈外面沒有變數i 會順便把變數i帶進webAPI裡面(webAPI是什麼請參見EventLoop這篇文章)
for迴圈跑一圈,就會包一個變數i進去webAPI,並跑一次EventLoop,接著就會在畫面上照順序印出 0 -> 1 -> 2