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


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

最佳實踐

  1. 使用 let / const 取代 var:確保每次迭代都有自己的區域變數。
  2. 保持閉包最小化:只捕獲必要的變數,避免把整個外層作用域帶入。
  3. 適時釋放資源:對於長時間持有的閉包,如計時器或事件監聽器,請在不需要時 clearTimeout、removeEventListener。
  4. 利用 ES6 模組:若可使用 import/export,可減少 IIFE 的使用,讓程式碼更易於管理。
  5. 檢測記憶體:在瀏覽器開發者工具的「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 開發的道路上更上一層樓,寫出更具可讀性、可維護性與效能的程式碼。祝你在程式之旅中玩得開心、寫得順利!