文章中英模式
布魯斯前端JS面試題目 - JavaScript 閉包(Closure)
從基礎理解 JavaScript 閉包(Closure)與 Lexical Scope,並搭配 useState 模型理解 React 如何記住 state,搭配記憶體洩漏案例與常見面試題,全面掌握閉包原理與陷阱。
文章中英模式
懶得看文章?那就來看影片吧
什麼是閉包?JavaScript 為什麼需要它?
閉包(Closure)是指一個函式和它定義時的外部變數的組合。即使函式被傳遞到不同的作用域執行,它仍然可以存取當時建立時的變數。
- •保存函式中的中間狀態(例如計數器)
- •封裝資料,實作私有變數
- •提供記憶性(如 memoization)
- •處理非同步流程與事件回呼
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
詞法作用域(Lexical Scope)是什麼?和閉包有什麼關係?
JavaScript 採用詞法作用域(Lexical Scope),也就是「函式可以存取的是它定義當下的變數環境,而不是它執行當下的環境」。
閉包就是這個機制的延伸,讓函式在未來仍然能記得定義當下的變數。
function outer() {
const name = 'Bruce';
function greet() {
console.log('Hello, ' + name);
}
return greet;
}
const sayHello = outer();
sayHello(); // Hello, Bruce
記憶體洩漏的真相:閉包用不好就變成地雷
閉包延長變數生命週期,若引用大型資料或DOM元素,又與長壽命結構綁定,會導致記憶體無法釋放。
範例 1:全域變數與 DOM 結合
let cache = {};
function bindElement(elementId) {
const element = document.getElementById(elementId);
cache[elementId] = () => {
console.log(element.innerText);
};
}
bindElement('myDiv');
DOM元素被閉包引用並存在全域cache中,即使元素被移除,GC也無法回收。解決:不需要時設置 cache[elementId] = null。
範例 2:setTimeout 長期引用變數
function setupLogger() {
let bigData = new Array(1000000).fill('*');
setTimeout(() => {
console.log(bigData[0]);
}, 10000);
}
計時器回調引用大型陣列,即使函式執行完畢,bigData仍保留在記憶體10秒。問題在於閉包讓setTimeout持續引用bigData,阻止垃圾回收。解決:清除計時器或只保留必要資料。
React 的 useState 為什麼能記住值?多次呼叫 Hook 為什麼需要閉包?
React 在每次 render 時會重新執行整個元件函式。但 useState 回傳的 getter/setter 是閉包,能記住對應狀態。
function simpleUseState(initialValue) {
let value = initialValue;
return [
() => value,
(newVal) => { value = newVal; }
];
}
const [getA, setA] = simpleUseState(1);
const [getB, setB] = simpleUseState(100);
setA(2);
console.log(getA()); // 2
console.log(getB()); // 100
🔥 常見面試題目
(一)什麼是閉包?JavaScript 為什麼需要它?
閉包是函式與其變數環境的組合,讓函式記住定義時的變數。JavaScript 用它來保存狀態、封裝資料和處理回調。
function createCounter() {
let count = 0; // 這個變數被閉包保存
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
(二)詞法作用域是什麼?與閉包的關係?
詞法作用域指變數範圍由定義位置決定,非執行位置。閉包基於此特性,讓函式帶著「出生環境」到處執行。
let name = 'Global';
function outer() {
let name = 'Outer';
function inner() {
console.log(name); // 使用的是 outer 的 name
}
return inner;
}
const innerFn = outer();
innerFn(); // 'Outer' - 即使在全域執行,仍記得定義時的環境
(三)如何用閉包實作私有變數?
閉包可創建私有變數,外部只能透過指定方法存取,實現類似物件導向的封裝概念。
function createSecret() {
let secret = '1234';
return {
get() { return secret; },
set(val) { secret = val; }
};
}
const user = createSecret();
console.log(user.get()); // '1234'
user.set('abcd');
console.log(user.get()); // 'abcd'
// console.log(secret); // 錯誤!外部無法直接存取
(四)React 的 useState 是怎麼利用閉包來記住值?多次調用 Hook 為什麼需要閉包?
每個 useState 創建獨立閉包,為每個狀態提供專屬「保險箱」,使狀態在重新渲染時保持獨立且不互相干擾。
function simpleUseState(initialValue) {
let value = initialValue;
return [
() => value,
(newVal) => { value = newVal; }
];
}
const [getA, setA] = simpleUseState(1);
const [getB, setB] = simpleUseState(100);
setA(2);
console.log(getA()); // 2
console.log(getB()); // 100
(五)閉包可能造成記憶體洩漏的情境有哪些?
閉包持續引用大型資料或DOM元素且未釋放時,會阻止垃圾回收,像忘關水龍頭般消耗資源。
- •全域變數綁定 DOM(造成元素無法被 GC)
- •setTimeout 或事件監聽綁定變數未清除
// 記憶體洩漏範例
function setupEventHandler() {
const largeData = new Array(10000).fill('data');
document.getElementById('button').addEventListener('click', function() {
console.log(largeData.length); // 持續引用大型資料
});
// 正確做法:保存引用並在適當時機移除
// const handler = function() { console.log(largeData.length); };
// document.getElementById('button').addEventListener('click', handler);
// 之後可以用 removeEventListener('click', handler) 移除
}
(六)工廠函式與閉包的關聯是什麼?
工廠函式利用閉包批量生產功能相似但參數不同的函式,每個函式都記住自己的獨立環境。
function createMultiplier(factor) {
return function(num) {
return num * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15