01 / 17
Chapter 4.3 · JavaScript 深度解析

Closure
閉包

實際運用

把狀態「關」在函式裡,做出安全又好用的介面——從封裝概念走向 HOF、debounce 與 memoize 的設計思維

01

Closure 核心原理

函式結束後,內部狀態為何仍存在

02

環境隔離與私有狀態

把資料藏起來,只暴露合法入口

03

Higher-Order Function

HOC、debounce、throttle 的設計思維

04

Memoize 與快取策略

同樣的輸入不要重算

Closure 核心

先用一個
最小例子
理解 Closure

run 裡宣告 a,把內層函式 logA 回傳出去而不是直接呼叫——

run() 明明已執行完,fn() 仍能印出 a。這代表 a 沒有消失,而是被 logA 「帶著走」

JAVASCRIPT

1function run() {
2  var a = 42;
3  function logA() {
4    console.log(a);
5  }
6  return logA; // 回傳,不呼叫
7}

8var fn = run();
9fn(); // ✓ 仍然印出 42
Closure 概念

Closure
到底在做什麼

01 · 建立環境

外層函式建立一個環境

包含函式執行時的所有變數與參數

02 · 引用環境

內層函式引用了外部變數

即使函式本體移到外部,引用關係仍然成立

03 · 保留環境

外層執行完,環境不會被回收

只要內層函式還存在,那段環境就繼續活著

04 · 受控存取

外部只能透過回傳的函式存取

無法直接觸碰環境裡的變數,只能走開的入口

狀態管理

為什麼
狀態外露
很危險

假設有一個 balance,提供 deduct 扣款且一次最多扣 10 元——

問題不在 deduct 寫得不好,而是 balance 在外面是公開的。任何人都可以直接改到它。

1var balance = 999;

2function deduct(n) {
3  balance -= Math.max(n, 10);
4}

5deduct(13); // 只扣 10 元(照規則)
6balance -= 999; // 💥 外部直接亂改

// 規則形同虛設
Closure 封裝

把狀態藏起來
只留合法入口

balance 放進 getWallet 裡,只回傳對外操作的方法。外界根本拿不到 balance,只能走 wallet.deduct()

balance -= 999;
// ReferenceError: balance is not defined

1function getWallet() {
2  var balance = 999;

3  return {
4    deduct(n) {
5      balance -= Math.max(n, 10);
6    },
7    getBalance() {
8      return balance;
9    }
10  };
11}

12var wallet = getWallet();
13wallet.deduct(13); // ✓ 只能走入口
設計思維

你得到的不只是技巧
而是一種設計方式

01

把資料藏起來(狀態私有化)

讓資料不在外部直接可見,外界無法偶然改動它

02

把行為公開(只暴露 API)

回傳一組你允許的操作方法,作為唯一的互動介面

03

讓外界只能在你規定的規則下操作狀態

所有合法操作都經過你設計的入口,規則不可繞過

歷史脈絡

IIFE:早期常見的
「隔離外界」寫法

在模組化還不普及的年代,用 IIFE 把程式包起來,避免把太多東西掛到 window。外面拿不到 secret,但可以用 window.myLib.api() 操作它。

Immediately Invoked 環境隔離
// 整段馬上執行,不污染全域
1(function () {
2  var secret = 42;

3  function api() {
4    console.log(secret);
5  }

6  window.myLib = { api };
7})();

// secret 拿不到
// window.myLib.api() 可以用
現代應用

Bundler 底層
也在大量使用 Closure

你平常用 webpack 打包,把輸出拆開來看,底層結構大致是:

整套機制之所以能運作,核心原因是:closure 讓 modulescacherequire 這些內部狀態被保留,且不會污染外部環境

II 第二章 · Higher-Order Function

幫函式加上功能

從 React HOC 到 debounce、throttle 與 memoize——把既有的東西包起來,然後加一層行為

HOF 動機

「到處都要加
一樣的邏輯」——
這就是問題所在

Class component 年代,「到處都要加一樣的檢查或行為」時:

更好的方式:把「權限檢查」抽成一個可重複使用的包裝層,讓元件本身只專注在顯示內容。

Higher-Order Component

HOC:用函式接收元件
回傳加強過的元件

INPUT

一個 Component

原本的元件,只專注在顯示內容

WRAP

額外行為

權限檢查、紀錄、埋點、錯誤處理

OUTPUT

加強版 Component

原元件邏輯不變,外層統一管理橫切邏輯

HOC 想解決的核心:不要讓橫切需求侵入每個元件內部。它本質上只是 Higher-Order Function 的一種應用。

Debounce

把「連續觸發」
變成「停下來才做」

使用者快速打字時,每打一字就呼叫 API 很浪費。設定 300ms:只要還在輸入就重設計時器,停止後才真的執行。

適合搜尋建議、即時輸入驗證
效果原本 10 次請求 → 變成 1 次
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 停止輸入 300ms 後才搜尋
const search = debounce(callAPI, 300);
input.addEventListener('input', search);
Throttle

把「觸發頻率」
限制在固定節奏

有些事件需要持續回饋(resize、scroll),但不希望每個事件都觸發昂貴計算。設定 50ms:不管多密集,固定間隔最多執行一次。

適合scroll 監聽、resize 重新排版
效果更新頻率穩定,效能可控
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// 每 50ms 最多執行一次
const onResize = throttle(recalc, 50);
window.addEventListener('resize', onResize);
比較

Debounce vs Throttle
怎麼選?

Debounce
等事件停止後再做

只要事件還在持續,就先暫停等待。停止後才執行一次。適合只需要最後結果的情境。

搜尋建議輸入驗證等停手才送出
Throttle
事件持續也定期做

不管事件多密集,固定時間內最多只執行一次,保持穩定節奏。適合需要持續回饋的情境。

scroll 監聽resize 排版拖曳互動
Memoize

把「算過的結果」留下來
下次直接用

控制的不是「什麼時候執行」,而是「同樣的輸入不要重算」。用參數當 key,把結果存在 Map,下次命中直接回傳快取。

  • 純函式且輸入輸出穩定的計算
  • 重複查詢、重複轉換資料的流程
  • 成本高且命中率高的運算
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

// 費氏數列:第二次起直接回傳
const fib = memoize(n =>
  n <= 1 ? n : fib(n-1) + fib(n-2));
核心洞見

它們都在做同一件事:包裝

HOC

包裝元件

加入權限等額外行為

Debounce / Throttle

包裝事件處理函式

控制觸發策略

Memoize

包裝計算函式

加入快取機制

共同核心

不改主體邏輯

在外面加一層,附加新能力

結語 · Chapter 4.3

Closure 是
「建立一個小世界
的能力

當你想在程式裡劃出一塊「只屬於我、外界別亂碰」的小天地時,closure 幾乎是最直接、也最可靠的選項。

橫切需求不要散落,集中在包裝層處理