Skip to content

2026-05-05

/ 11 分鐘閱讀

/ JavaScript Hard Parts v3

Closure:為什麼每次呼叫外層函式都是全新開始

用來產生閉包的函式並沒有記憶

理解 closure 時最容易產生的誤解:認為 newFuncanotherFunction 在執行時,某種程度上仍與 outer 保持著連結,或是每次執行會「回去找 outer」重新設定資料。

事實上:

  • outer 本身是一個普通函式,每次執行都是全新的開始,執行完畢後完全忘記自己做過什麼
  • newFuncanotherFunction 在被指派之後,與 outer 再無任何關係
  • 它們持有的只是當初那次 outer 執行時,add1 被定義當下所綁定的資料快照

同樣用下方的程式碼來做範例。

javascript
function outer() {
    let counter = 0;
    function add1() {
        counter++;
    }
    return add1;
}

const newFunc = outer();
newFunc();
newFunc();

const anotherFunction = outer();
anotherFunction();
anotherFunction();

第二次呼叫 outer 時,之所以 counter0 開始,不是因為有任何「重置」機制,而是因為這是一次全新的執行環境,在這次執行中,let counter = 0 被重新執行了一次,產生了一個全新的 counter = 0 儲存在電腦記憶體中,並隨著這次的 add1 一起被帶出去。

函式執行時的兩層記憶體

每次呼叫 newFuncanotherFunction,都同時擁有:

記憶體每次呼叫時特性
本地記憶體全新建立,執行完畢即清除暫時性
背包(Closure)持續存在,跨越每次呼叫私有且持久

這份背包資料是私有的:它不在全域,外部無法直接存取,只有持有它的函式在執行時能夠讀取和修改。

關於記憶體釋放

只要持有函式的標籤(變數)仍然存在,背包就不會被釋放。若要讓垃圾回收機制(garbage collection)清除背包,可以將該變數重新指派:

javascript
anotherFunction = null; // 解除參考,背包資料最終會被 GC 清除

值得注意的是,這並不構成真正的「記憶體洩漏(memory leak)」。記憶體洩漏的定義是:資料無法再被存取,但仍佔用空間。而背包中的資料只要函式還在,就仍可透過呼叫該函式來存取,所以不符合定義。JavaScript 也會自動優化——只有在函式中實際被參照的變數才會進入背包,未被參照的會被垃圾回收掉。

延伸:IIFE 與 Module Pattern 的雛形

既然 outer 回傳的函式可以攜帶私有的持久資料,那麼可以更進一步,讓 outer 在定義後立刻執行(Immediately Invoked Function Expression,IIFE):

javascript
const newFunc = (function outer() {
    let counter = 0;
    function add1() {
        counter++;
    }
    return add1;
})();

這正是 Module Pattern(模組模式) 的核心原理:用一個立即執行的外層函式建立一個私有的持久狀態空間,回傳的函式可以持續存取並操作這份狀態,而外部無法直接干涉。

複習

當一個函式從外層函式被回傳並儲存在變數中時,外層函式執行環境中的本地變數會怎樣?

被回傳的函式會將其定義當下所在位置的活資料,以背包(closure)的形式一起帶出來。 即使外層函式的執行環境已被銷毀,這份資料仍持續存在, 讓被回傳的函式在執行時能夠存取這些變數。

如果外層函式被呼叫多次,每次各回傳一個函式,這些回傳的函式是否共享同一份背包資料?

不,每次呼叫外層函式都會建立全新的執行環境,擁有各自獨立的本地記憶體。 每個被回傳的函式都會得到自己專屬的背包,其中包含來自各自執行環境的資料。 這些回傳的函式彼此之間,以及與外層函式之間,在回傳後都不存在任何關係。

帶有背包(closure)的函式執行時,其本地執行環境是否會在多次呼叫之間持續保留?

不,每次函式執行完畢後,本地執行環境都會被清空。 但背包(closure)中的資料會跨越多次呼叫持續存在。 函式查找變數的順序是:先查本地記憶體,再查背包,最後才是全域作用域。

當內層函式被回傳並儲存在變數中之後,它與外層函式之間是什麼關係?

兩者之間沒有任何持續的關係。 被回傳的函式不會再回頭參照外層函式,它只是持有外層函式執行環境在回傳當下的資料快照。 外層函式本身在多次執行之間也不保有任何記憶。

在程式結束之前,如何釋放函式背包(closure)所佔用的記憶體?

若將持有該函式的變數重新指派為其他值(例如 null), 垃圾回收機制(garbage collection)最終會釋放背包中的資料。 只要變數仍持有對該函式的參照,背包資料就會持續存在於記憶體中。

小測驗

函式執行完畢後,其本地記憶體會怎樣? 被清除丟棄
當一個函式從另一個函式中被回傳時,它會額外攜帶什麼資料? 它被定義當下所在環境的周圍資料
附加在被回傳函式上的持久私有記憶體,通常被稱為什麼? 背包(Backpack)
outer 被多次呼叫時,它是否會在多次執行之間保有記憶? 不會,每次呼叫都會建立全新的執行環境
如果將一個持有回傳函式的變數重新指派為 null,其背包資料最終會怎樣? 被垃圾回收機制(garbage collection)釋放

此文章是 FrontendMasters 上的 The Hard Parts of JavaScript 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 16 篇 / 共 27 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash