[JavaScriptWeird]No.34 瞭解閉包(一)

前言

這個系列會拆分成一跟二,主要介紹 JavaScript 內其中一個蠻惡名昭彰的觀念,這是一個相當抽象且不好懂的觀念,不過如果前面基礎有踏穩,其實就是之前的執行環境、範圍鏈、一級函式、執行堆等等的延伸應用在加入一點新觀念而已,課程到這邊也已經過50%了,勉勵自己繼續加油!

閉包 (Closuere)

閉包的解釋網路上有蠻多的,但我特別喜歡卡斯伯的說法,比較適合我的金魚腦,哈!

透過範例了解閉包過程

1
2
3
4
5
6
function greet(whatToSay){  
return function(name){
console.log(whatToSay + ' ' + name);
}
}
greet('Hi')('Tony'); // Hi Tony

這個寫法很有趣,我們呼叫 greet 函式,然後回傳匿名函式,因此也可以直接在後面又接一個 () ,立即呼叫內部函式。

然而內部函式雖然沒有 whatToSay 變數,但因為範圍鏈,會轉而尋找外部環境內的 whatToSay 變數。

這樣看來,都還在認知的合理範圍,但其實有點不尋常,我們修改一下:

1
2
3
4
5
6
7
8
function greet(whatToSay){  
return function(name){
console.log(whatToSay + ' ' + name);
}
}

var sayHi = greet('Hi');
sayHi('Tony'); // Hi Tony

用變數 sayHi 指向 greet 函式的位址,並且呼叫內部函式。

但,為什麼 sayHi 函式仍然知道 whatToSay 是什麼?

問題點在於

  • greet 函式 被呼叫,執行回傳匿名函式後,函式結束並離開執行堆。
  • 為什麼被創造在 greet 函式內的變數 whatToSay ,在離開執行堆後,仍然可以被內層匿名函式取用?

這是有可能的,這就是閉包的概念,讓我們用圖片了解:

  1. 當程式開始時,全域執行環境被創造。
    課程截圖
    課程截圖

  2. sayHi = greet('Hi') 這行時, greet 函式被呼叫,並且 whatToSay 變數被傳入到 greet 函式的變數環境。

  3. greet 函式執行完畢,回傳一個匿名函式, greet 函式的執行環境離開執行堆。

每個執行環境都有專屬的記憶體空間,變數與函式都被創造在內。而當執行環境沒了之後,記憶體空間會如何?

一般情況下,JavaScript 會清除掉記憶體空間,這動作稱為垃圾回收 (Garbage Collection)。

但當閉包的執行環境結束後,記憶體空間仍然存在。

  1. 現在我們回到全域執行環境,接著呼叫 sayHi 指向的函式,然後又創造一個新的執行環境。

  2. 我們傳入了變數 name 的值,所以這會被保存在記憶體裡,但當執行到 console.log 這行時,JavaScript 發現這裡找不到 whatToSay ,因此根據範圍鍊,跑到了外部環境去尋找。

    即使外層的執行環境已經離開執行堆, sayHi 的執行環境仍然可以參考到在外部環境記憶體空間的 whatToSay 變數。

執行環境可以把自身環境內會用到的外部變數整個包住。
意思是那些在執行過程中應該要被參考到的變數會整個被執行環境包住,即使執行環境已經不存在了。

然而這個包住所有可以取用變數的現象稱為閉包。

閉包是 JavaScript 本身的特色,無關於什麼時候呼叫函式,不需要擔心這個函式的外部環境是否還在執行,JavaScript 會確保無論正在執行哪個函式,都能取用到應該要取用的變數。這還是按照範圍鏈的規則在走,並沒有改變。我們下一篇將記錄一些經典的例子。

延伸閱讀

0%