什麼是閉包 (Closure)?
2025年4月2日
閉包 (closure) 是程式語言的一種特性,在 JavaScript 中也扮演相當重要的角色,被廣泛應用在 JavaScript 程式庫中。許多被開發者大量使用的重要功能,也都看得到閉包的身影,舉例來說最熱門的 JavaScript 函式庫 React 中的 useState
就是透過閉包來實作。
以面試的角度來說,不僅僅要了解什麼是閉包,同時也要知道閉包會有什麼應用的情景。假如你目前仍不熟閉包的概念,或不確定可以怎麼應用,千萬要在面試前弄熟。
什麼是閉包?
在 MDN 文件中,閉包被定義為函式以及該函式被宣告時所在的作用域環境 (lexical environment) 的組合。白話一點說,閉包就是內部函式能夠取得函式外部的變數,並且記住這個變數。因為能夠記住這個外部變數,閉包很常被用來做狀態保存。
以下是一個最簡單的例子,在下方程式碼中的 inner
函式,能拿到外部函式 outer 的 a
變數,並將其保存在記憶體中。當我們呼叫 inner
時,之所以不是每次都回傳 1
,而是回傳 1
、2
、3
不斷加上去,正是因為之前的 a
的狀態被記住了。
function outer() {
let a = 0;
function inner() {
a += 1;
console.log(a);
}
return inner;
}
const inner = outer();
inner(); // 1
inner(); // 2
inner(); // 3
我們可以理解成:閉包這種特性,可以讓我們在一個內層函式中 (這邊的inner
),訪問到外部函式的作用域 (這邊的outer
),並且會記住外部函式的變數(這邊的a
)。
在了解閉包是什麼後,接著我們來看看閉包的實際用途。
閉包的應用 1 — 狀態保存
在寫程式時,我們很常會需要記住某個狀態,JavaScript 的熱門函式庫 React 就有提供一個 useState
讓開發者來管理狀態。以下我們模擬一個簡化版的 useState
,可以在下方的程式碼看到,getState
與 setState
作為內部函式,可以取得外部函式當中的 state,在實際呼叫後,如果這個 state 有改變,getState
可以持續取得最新改變的值。
// 因為閉包的關係,getState 與 setState 可以取得與記得 state
function useState(initialState) {
let state = initialState;
function getState() {
return state;
}
function setState(updatedState) {
state = updatedState;
}
return [getState, setState];
}
const [count, setCount] = useState(0);
count(); // 0
setCount(1);
count(); // 1
setCount(500);
count(); // 500
又或者先前 React 核心團隊成員 Sebastian Markbåge 分享的一段程式碼,在說 React 的 Server Actions 中,可以運用閉包來做版本檢查。下面這段程式碼 verifiedVersion
記住的是在首次渲染時拿到的版本,因為閉包的關係,內部的函式 publish
函式,能取得 verifiedVersion
這時,如果要做版本檢查,可以在 publish
裡面再呼叫一次 await getVersion
,拿到當下的版本。這時就可以比較首次渲染時的版本,以及當下的版本,並當版本不同時,可以做處理 (這邊是返回一個錯誤訊息)

閉包的應用 2 — 快取
還記得在 什麼是高階函式 (Higher order function)?使用高階函式有什麼好處? 一文中,我們有談到在面試中很常出現的 LeetCode 2623 題 ,須要去實作一個 memoize
函式。
具體來說,題目的要求是,當今天有一個函式,被 memoize
這個高階函式包過後,接下來使用該函式時,如果有相同的引數輸入,第二次開始就不會重新進行運算,而是會直接回傳先前運算過的結果。
// 舉例來說,有個 sum 函式,會回傳兩個參數的相加
const sum = (a, b) => a + b;
// 今天如果被 memoize 後獲得 memoizedSum,會有以下的作用
const memoizedSum = memoize(sum);
memoizedSum(2, 2); // 4 這是經過運算的
memoizedSum(2, 2); // 4 這是直接從快取拿的,不用再次運算
在 什麼是高階函式 (Higher order function)?使用高階函式有什麼好處? 一文中,我們是從高階函式的角度切入,談說像 memoize
這類高階函式,接收了其他函式後,為該函式賦予功能,然後再回傳賦予功能後的版本。
當時我們沒有細談 memoize
的實作,因此讓我們在這邊進一步來談。在下面我們有一個簡易版本的 memoize
實作,附上註解讓讀者們可以理解。
function memoize(fn) {
// 聲明一個 cache 物件,透過 cache 來放快取的東西
// 因為閉包的緣故,下面回傳的函式可以存取到這個 cache 變數
const cache = {};
// 透過擴展運算符,拿到引數
return (...args) => {
// 將引數當作快取的 key
const key = JSON.stringify(args);
// 查看現在的快取有沒有這個 key,有的話就不用再次運算,直接回傳
if (key in cache) {
return cache[key];
} else {
// 沒有的話,就把收到引數帶入,運算出結果
const val = fn(...args);
// 把結果放入快取,下次有同樣的 key 就不用重新運算
cache[key] = val;
return val;
}
};
}
在看完 memoize
函式的實作,相信大家有在註解中讀到「因為閉包的緣故,回傳的函式可以存取到這個 cache 變數」。
以 memoize
的例子,const memoizedSum = memoize(sum);
這一行當中,memoizedSum
是把 sum
輸入到 memoize
後得到的回傳結果,也就是return (...args) => { // 內容省略 }
這一個函式。
而因為閉包的緣故,memoizedSum
這個被回傳的函式,能夠記得且持續使用 cache
這個變數,進而讓我們能實作出快取的功能。
閉包的應用 3 — 模擬私有變數
許多程式語言有宣告私有方法的語法,這些私有變數對於外部來講是隱藏的,這是一項很重要的特性,因為有時候我們在開發的程式碼內部細節,並不想讓外部來獲取。JavaScript 並不支援私有變數,但我們可以透過閉包做出類似的功能。如下方程式碼範例:
// privateCounter 沒被法被外部修改,
// 因為閉包的關係 increment 與 decrement 可以存取到 privateCounter
// 因此 privateCounter 只能夠透過 increment 與 decrement 來改,這能有效避免被誤觸到
var counter = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
})();
console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1
閉包缺點 — 記憶體洩漏
在看完上面對於閉包的解說,你可能會覺得閉包真是一個好用的功能,應該善加利用。這樣說沒錯,只是如同軟體工程領域的各項技術,都不會只有好處,而是存在取捨,閉包也一樣。
當使用閉包時,雖然可以獲得「記得」外在變數的好處,但是「記得」這件事本身也是有代價的,具體來說的代價會是佔用記憶體,甚至有潛在的記憶體洩漏問題。
還記得我們在 如何避免前端系統的記憶體洩漏 (memory leak)? 一文談到的,當今天每新增一個變數,都會需要佔用記憶體的空間。由於記憶體不是無限的,當今天記憶體不夠用,就需要手動清除記憶體,或是如果程式語言本身有垃圾回收機制 (garbage collection),會啟動這個機制來釋放記憶體。以前端來說,當垃圾回收機制啟動過於頻繁,將可能導致頁面的性能不佳,進而會出現卡頓的狀況。
以下面的例子來說,longArray
沒有被使用到,但是因為閉包的原因會一直被addNumbers
記住。假如今天longArray
有被使用,那就沒問題,但因為它沒有被用到但仍存在於記憶體中沒被清除,這種情況就是典型的記憶體洩漏。
function outer() {
const longArray = [];
return function inner(num) {
longArray.push(num);
};
}
const addNumbers = outer();
for (let i = 0; i < 100000000; i++) {
addNumbers(i);
}
因此推薦在使用閉包時仍需要有意識地使用,確保只有真的需要時才用,以避免不必要的記憶體被佔著,白白浪費寶貴的空間。
閱讀更多
在談完以上的流程,接下來我們會進一步談閉包的延伸概念 — 柯里化 (currying),這是在函式程式設計中很重要的概念,也是實務工作與面試很常會遇到的。 關於柯里化、其他函式程式設計的深入內容,我們在 E+ 成長計畫的主題文都有更詳細談到,推薦感興趣的讀者閱讀。
本文為 E+ 成長計畫的深度內容,截取段落開放免費閱讀。歡迎加入 E+ 成長計畫閱讀完整版本 (點此了解 E+ 的詳細介紹)。