JavaScript 函式(Functions)
主題:閉包(Closure)
簡介
在 JavaScript 中,函式是第一等公民(first‑class citizens),它們不只可以被當作變數傳遞,還可以在執行時「捕獲」外部環境的變數。這種能力稱為 閉包(Closure),是語言最具威力的特性之一。
閉包讓我們能夠在函式執行完畢後,仍保留對其外部作用域的存取權限,從而實現 資料封裝、模組化、私有變數、柯里化(currying) 等常見需求。對於初學者而言,了解閉包的運作機制是邁向中階、甚至高階 JavaScript 開發者的關鍵一步。
本篇文章將以 繁體中文(台灣) 為基礎,從概念說明、實作範例、常見陷阱到最佳實踐,完整且深入淺出地介紹閉包,讓你在實務開發中能夠自信運用。
核心概念
1. 什麼是閉包?
閉包 = 函式 + 它在宣告時所能存取的外部變數環境
簡單來說,當一個函式在其外部作用域(Lexical Environment)中被建立時,JavaScript 會同時為它建立一個「閉包」物件,記錄下當時可見的變數。即使該函式之後被傳遞到其他地方執行,或是外層作用域已經結束,這些變數仍然會被保留。
function outer() {
let count = 0; // outer 的局部變數
return function inner() {
// inner 形成了對 count 的閉包
count++;
console.log(count);
};
}
const counter = outer(); // 呼叫 outer,得到 inner 函式
counter(); // 1
counter(); // 2
在上例中,inner 雖然在 outer 執行完畢後仍被呼叫,但它仍能存取 outer 的 count 變數,這正是閉包的核心。
2. 為什麼要使用閉包?
| 需求 | 透過閉包的解決方式 |
|---|---|
| 私有成員 | 將變數封裝在函式內部,只能透過提供的介面存取 |
| 資料共享 | 多個函式共享同一個「私有」狀態 |
| 延遲執行 | 把需要的資料先保存起來,稍後再執行 |
| 函式工廠 | 產生具有特定行為的函式(如 add(5) 產生加 5 的函式) |
3. 閉包的形成時機
- 函式宣告或表達式:每一次函式被建立,都會捕獲當時的外部環境。
- 函式被返回或傳遞:只要函式離開其原始作用域,仍被外部引用,就會形成閉包。
- 執行階段:閉包的變數在 執行階段(runtime)保持活躍,直到沒有任何引用指向它們,才會被垃圾回收(GC)。
4. 程式碼範例
以下提供 5 個實用範例,從基礎到進階,說明閉包在不同情境的應用。
範例 1️⃣:簡易的計數器(私有變數)
function createCounter() {
let value = 0; // 私有變數
return {
inc() { value++; return value; },
dec() { value--; return value; },
get() { return value; }
};
}
const counter = createCounter();
console.log(counter.inc()); // 1
console.log(counter.inc()); // 2
console.log(counter.dec()); // 1
console.log(counter.get()); // 1
說明:
value只在createCounter的閉包內可見,外部無法直接修改,達到封裝效果。
範例 2️⃣:函式工廠 – 加法產生器(柯里化)
function add(x) {
// 回傳一個新的函式,形成閉包保存 x
return function(y) {
return x + y;
};
}
const add5 = add(5);
const add10 = add(10);
console.log(add5(3)); // 8
console.log(add10(7)); // 17
說明:
add產生的每個子函式都保有自己的x,這正是閉包的典型應用。
範例 3️⃣:延遲執行 – 防抖 (debounce)
function debounce(fn, wait) {
let timer; // 閉包保存 timer
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
// 使用範例:避免使用者快速鍵入時觸發過多 AJAX
const onSearch = debounce(query => {
console.log('搜尋關鍵字:', query);
}, 300);
window.addEventListener('input', e => onSearch(e.target.value));
說明:
timer變數被debounce返回的函式持有,使得每次呼叫都能清除先前的計時器,達到防抖效果。
範例 4️⃣:模組化 – 私有函式與公開介面
const MathModule = (function() {
// 私有變數與函式
const PI = 3.1415926;
function square(x) { return x * x; }
// 公開介面
return {
areaOfCircle(r) { return PI * square(r); },
circumference(r) { return 2 * PI * r; }
};
})();
console.log(MathModule.areaOfCircle(3)); // 28.2743334
console.log(MathModule.circumference(3)); // 18.8495558
說明:整個 IIFE(立即執行函式)形成閉包,
PI與square成為私有成員,外部只能透過返回的物件存取。
範例 5️⃣:迭代器 – 產生無限序列
function* naturalNumbers() {
let n = 0;
while (true) {
yield n++; // 每次 yield 前,n 都被閉包捕獲
}
}
const gen = naturalNumbers();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
說明:Generator 函式本身即是一個閉包,
n的狀態會在每次next()呼叫之間保留下來,形成「懶評估」的序列。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 變數共享(Loop Closure) | 在 for 迴圈內使用 var,所有回呼都指向同一個變數,導致結果錯誤。 |
使用 let(塊級作用域)或立即執行函式 (IIFE) 捕獲當前值。 |
| 記憶體洩漏 | 閉包持有大量不再需要的資料,導致 GC 無法釋放。 | 確保不再需要時,手動將引用設為 null,或避免在閉包內保存大型物件。 |
| 過度使用 | 為了「炫技」而濫用閉包,導致程式碼難以閱讀與維護。 | 僅在需要封裝、延遲執行或資料共享時使用,保持簡潔。 |
this 失效 |
閉包內的 this 會被固定為建立時的環境,導致方法呼叫錯誤。 |
使用箭頭函式(箭頭函式不綁定自己的 this)或在閉包外先保存 const self = this。 |
| 多層閉包 | 深層嵌套的閉包會增加呼叫成本與除錯難度。 | 盡量將邏輯抽離成獨立函式或使用模組化結構。 |
最佳實踐
- 使用
let/const取代var:確保每次迭代都有自己的區域變數。 - 保持閉包最小化:只捕獲必要的變數,避免把整個外層作用域帶入。
- 適時釋放資源:對於長時間持有的閉包,如計時器或事件監聽器,請在不需要時
clearTimeout、removeEventListener。 - 利用 ES6 模組:若可使用
import/export,可減少 IIFE 的使用,讓程式碼更易於管理。 - 檢測記憶體:在瀏覽器開發者工具的「Memory」面板觀察閉包是否被正確釋放。
實際應用場景
| 場景 | 為何適合使用閉包 | 範例簡述 |
|---|---|---|
| 表單驗證 | 每個欄位的驗證規則可以封裝在閉包內,避免全域變數衝突。 | const emailValidator = (() => { const regex = /.../; return value => regex.test(value); })(); |
| 自訂 UI 元件 | 內部狀態(如開關、滑桿值)可透過閉包保持,外部僅暴露 API。 | Vue/React 中的 useState 本質上是閉包實作。 |
| API 請求防抖/節流 | 防止使用者過度點擊,透過閉包保存計時器或上一次呼叫時間。 | 前述 debounce、throttle 函式。 |
| 遊戲開發 | 角色屬性、分數等需要持久化的狀態,使用閉包避免全域污染。 | function createPlayer(name){ let score=0; return { getScore:()=>score, addScore:n=>{score+=n;}}} |
| 函式式程式設計 | 高階函式(如 map, filter, reduce)常返回閉包以實作延遲計算。 |
array.map(item => item * factor) 中的 factor 會被閉包捕獲。 |
總結
- 閉包 是 JavaScript 函式在建立時自動捕獲外部變數環境的機制,允許函式在執行完畢後仍能存取這些變數。
- 它是實現 私有屬性、函式工廠、延遲執行、模組化 等重要概念的基礎。
- 使用時要留意 迴圈閉包、記憶體洩漏、
this的綁定 等常見陷阱,並遵守 最小化閉包、適時釋放資源 的最佳實踐。 - 在 表單驗證、UI 元件、API 防抖、遊戲開發 等實務場景中,閉包提供了簡潔且安全的解決方案。
掌握閉包的原理與應用,將讓你在 JavaScript 開發的道路上更上一層樓,寫出更具可讀性、可維護性與效能的程式碼。祝你在程式之旅中玩得開心、寫得順利!