本文 AI 產出,尚未審核

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 執行完畢後仍被呼叫,但它仍能存取 outercount 變數,這正是閉包的核心。


2. 為什麼要使用閉包?

需求 透過閉包的解決方式
私有成員 將變數封裝在函式內部,只能透過提供的介面存取
資料共享 多個函式共享同一個「私有」狀態
延遲執行 把需要的資料先保存起來,稍後再執行
函式工廠 產生具有特定行為的函式(如 add(5) 產生加 5 的函式)

3. 閉包的形成時機

  1. 函式宣告或表達式:每一次函式被建立,都會捕獲當時的外部環境。
  2. 函式被返回或傳遞:只要函式離開其原始作用域,仍被外部引用,就會形成閉包。
  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(立即執行函式)形成閉包,PIsquare 成為私有成員,外部只能透過返回的物件存取。


範例 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
多層閉包 深層嵌套的閉包會增加呼叫成本與除錯難度。 盡量將邏輯抽離成獨立函式或使用模組化結構。

最佳實踐

  1. 使用 let / const 取代 var:確保每次迭代都有自己的區域變數。
  2. 保持閉包最小化:只捕獲必要的變數,避免把整個外層作用域帶入。
  3. 適時釋放資源:對於長時間持有的閉包,如計時器或事件監聽器,請在不需要時 clearTimeoutremoveEventListener
  4. 利用 ES6 模組:若可使用 import/export,可減少 IIFE 的使用,讓程式碼更易於管理。
  5. 檢測記憶體:在瀏覽器開發者工具的「Memory」面板觀察閉包是否被正確釋放。

實際應用場景

場景 為何適合使用閉包 範例簡述
表單驗證 每個欄位的驗證規則可以封裝在閉包內,避免全域變數衝突。 const emailValidator = (() => { const regex = /.../; return value => regex.test(value); })();
自訂 UI 元件 內部狀態(如開關、滑桿值)可透過閉包保持,外部僅暴露 API。 Vue/React 中的 useState 本質上是閉包實作。
API 請求防抖/節流 防止使用者過度點擊,透過閉包保存計時器或上一次呼叫時間。 前述 debouncethrottle 函式。
遊戲開發 角色屬性、分數等需要持久化的狀態,使用閉包避免全域污染。 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 開發的道路上更上一層樓,寫出更具可讀性、可維護性與效能的程式碼。祝你在程式之旅中玩得開心、寫得順利!