
結論先講
Closure 不是面試題。你寫 React Hook、setInterval、事件處理器、資料快取每天都在用 closure,只是你不知道。
Closure 的本質一句話:函式「記得」它被定義時的外部變數,即使函式被搬到別的地方執行。
這句話不懂?看下去。
Scope:變數能被看到的範圍
JavaScript 有三種 scope:
const globalVar = 'global'; // Global Scope
function outer() {
const functionVar = 'function'; // Function Scope
if (true) {
const blockVar = 'block'; // Block Scope(let/const)
console.log(globalVar, functionVar, blockVar); // 全看得到
}
console.log(blockVar); // ❌ ReferenceError
}關鍵概念:Lexical Scope(詞法作用域)
函式能看到什麼變數,是根據它「寫在哪」決定的,不是它「在哪被執行」。
function outer() {
const x = 10;
function inner() {
console.log(x); // inner 看得到 x,因為 inner 寫在 outer 裡面
}
return inner;
}
const fn = outer();
fn(); // 印出 10 — 即使 outer 已經執行完,inner 仍記得 x就算 outer 已經跑完,inner 還是記得 x = 10。這就是 closure。
Closure 的三個實用場景
場景 1:資料私有化
JS 沒有 private 關鍵字(class private field 是新東西)。用 closure 做:
function createCounter() {
let count = 0; // 外部看不到
return {
increment: () => ++count,
get: () => count,
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.get(); // 2
counter.count; // undefined(外部存取不到)count 被 closure「包住」。
場景 2:React Hook 的本質
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
// ↑ 這個 closure 記得 count
return { count, increment };
}每次 re-render,useCounter 重跑,產生新的 count、新的 increment closure。舊的 closure 還「記得」舊的 count。
這就是為什麼 hook 有「stale closure」問題(下面會講)。
場景 3:事件處理器延遲執行
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 印 0, 1, 2(用 let)for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 印 3, 3, 3(用 var)差在哪?let 是 block scope,每次迴圈產生新的 i,每個 closure 記得自己的 i。
var 是 function scope,三個 closure 共用同一個 i,迴圈跑完 i 變 3,三個都印 3。
這題是面試常考題,現在你知道背後機制了。
Stale Closure:React 最常見的 bug
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永遠印 0
setCount(count + 1); // 永遠設成 1
}, 1000);
return () => clearInterval(id);
}, []); // 空依賴陣列
return <div>{count}</div>;
}count 永遠是 0,setCount 每次都設成 1。為什麼?
useEffect 只在 mount 時跑一次(空依賴),裡面的 closure 記得那次的 count = 0。往後即使 state 更新,closure 還是抓舊的 count。
解法 1:用 functional updater
setCount(c => c + 1); // 用上次的值,不依賴 closure解法 2:把 count 放進依賴
useEffect(() => {
const id = setInterval(() => {
console.log(count);
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]); // 每次 count 變了,重建 interval但這會每秒重建 interval,效能不好。
解法 3:用 ref 避開 closure
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // ref 永遠拿最新值
}, 1000);
return () => clearInterval(id);
}, []);這三招你都該會。
Closure 造成記憶體洩漏
Closure 會「抓住」外部變數,JS 的 GC 不會回收被抓住的變數。
洩漏例子
function setupListener() {
const hugeData = new Array(1000000).fill('data'); // 很大的資料
document.getElementById('btn').addEventListener('click', () => {
console.log('clicked');
// closure 記得 hugeData,就算沒用到
});
}
setupListener();
// hugeData 永遠不會被 GC,因為 listener 還活著解法:明確釋放
let hugeData = new Array(1000000).fill('data');
const handler = () => console.log('clicked');
button.addEventListener('click', handler);
// 用完:
button.removeEventListener('click', handler);
hugeData = null; // 讓 GC 能回收React 裡的洩漏模式
useEffect(() => {
const bigData = fetchBigData();
const handler = () => {
// 用到 bigData
};
window.addEventListener('scroll', handler);
return () => {
window.removeEventListener('scroll', handler);
// 不 remove,組件卸載後 bigData 還被 handler 抓著
};
}, []);cleanup function 必寫。
Module Scope(ESM)
ES Module 有自己的 module scope,不是 global:
// utils.js
const secret = 'hidden';
export function getSecret() { return secret; }secret 只在 utils.js 裡看得見。其他檔案要取得必須透過 getSecret()。
這也算一種 closure — module 內的函式記得 module scope 的變數。
IIFE(立即執行函式):Closure 的古老用法
ES6 之前沒有 let,用 IIFE 造 block scope:
(function () {
const privateVar = 'hidden';
// 外面看不到 privateVar
})();現代 JS 用 { ... } 配 let 就好,IIFE 是歷史產物。知道就好,不用寫。
實戰 Checklist
- 搞懂 Lexical Scope:函式記得「被寫在哪」的外部變數
- React Hook 的依賴陣列搞錯 → stale closure
- 用
setCount(c => c + 1)避免 stale closure -
useRef存「永遠最新值」的東西 - Event listener 用完要
removeEventListener -
useEffect的 cleanup function 必寫 - 迴圈裡的 async callback 用
let,不要用var
相關文章
- JS 子 Roadmap
- 下一篇 → Event Loop(🌱)
- React Hooks 基礎
- Frontend Roadmap