JavaScriptクロージャ完全実践ガイド〜スコープ・this・カリー化・モジュール・実用パターン50選【2026年版】〜

JavaScriptを学んでいて「クロージャ(closure)」という単語にぶつかった瞬間、急に話が抽象的になって挫折した人は多いはずです。しかし実務では、useStateの内部実装、デバウンス・スロットル、メモ化キャッシュ、モジュールパターン、カリー化、Reactのカスタムフック、Reduxのreducer合成など、クロージャを理解していないと書けないコードだらけです。

この記事では「クロージャとは何か」を曖昧な比喩ではなく、30個以上のコピペで動くES2025準拠コードで徹底的に体感してもらいます。レキシカルスコープの根本から、プライベート変数、関数型のcompose/pipe、Reactの内部実装、TypeScriptでの型付け、メモリリークの罠まで、現場で必要な「クロージャの全部」を1記事で押さえます。

  1. 1. クロージャの定義とレキシカルスコープ
    1. 1-1. レキシカルスコープの最小例
    2. 1-2. クロージャは「関数 + スコープ」のセット
    3. 1-3. クロージャが「実体」を持つことの確認
  2. 2. 関数スコープ vs ブロックスコープ・var/let/constの挙動差
    1. 2-1. var は関数スコープ
    2. 2-2. let / const はブロックスコープ
    3. 2-3. ループ内 setTimeout の有名な罠
    4. 2-4. let で解決
    5. 2-5. IIFE で var の罠を解決する古典手法
  3. 3. プライベート変数とカウンター実装
    1. 3-1. シンプルなカウンター
    2. 3-2. プライベートメソッド付きの実装
    3. 3-3. ID発番器
  4. 4. メモ化・キャッシュ・デバウンス・スロットル
    1. 4-1. シンプルなメモ化
    2. 4-2. 多引数対応メモ化
    3. 4-3. LRUキャッシュ
    4. 4-4. デバウンス(debounce)
    5. 4-5. スロットル(throttle)
    6. 4-6. once(一度だけ実行)
  5. 5. 関数型プログラミングの基礎パターン
    1. 5-1. 手動カリー化
    2. 5-2. 汎用 curry 実装
    3. 5-3. partial application
    4. 5-4. compose(右から左へ合成)
    5. 5-5. pipe(左から右へ合成)
    6. 5-6. Higher-Order Function(高階関数)
  6. 6. モジュールパターンとIIFE
    1. 6-1. クラシックなモジュールパターン
    2. 6-2. リビーリングモジュールパターン
    3. 6-3. シングルトンの実装
  7. 7. React・Reduxの内部実装をクロージャで再現する
    1. 7-1. useState のミニ実装
    2. 7-2. 複数useStateを扱う配列ベース実装
    3. 7-3. createStore(Reduxの本体)
    4. 7-4. カスタムフック風に状態を保持
    5. 7-5. useRef のミニ実装
  8. 8. クロージャと this、async、TypeScript
    1. 8-1. クロージャと this(アロー関数の活用)
    2. 8-2. bindとクロージャの違い
    3. 8-3. クロージャ × async
    4. 8-4. リトライ付き fetch
    5. 8-5. TypeScript でのクロージャ型付け
    6. 8-6. ジェネリックなメモ化
  9. 9. メモリリーク・GC・アンチパターン
    1. 9-1. クロージャがGCを止める例
    2. 9-2. リーク対策版
    3. 9-3. addEventListener の解除忘れ
    4. 9-4. setIntervalのリーク
    5. 9-5. アンチパターン: クロージャの乱用
    6. 9-6. パフォーマンス: 高頻度呼び出しでクロージャを毎回作らない
    7. 9-7. WeakMapで参照を弱く保つ
  10. 10. 実用パターン集とまとめ
    1. 10-1. リソースプール
    2. 10-2. パイプライン処理
    3. 10-3. イベントエミッタ
    4. 10-4. 状態機械(State Machine)
    5. 10-5. レート制限(Rate Limiter)
    6. 10-6. 関数キャンセル(AbortController風)
  11. 11. クロージャを学んだ後のキャリア戦略
  12. 12. まとめ

1. クロージャの定義とレキシカルスコープ

クロージャを一言で言えば「関数が、自分の外側のスコープにある変数を、関数が呼ばれるたびに参照し続ける仕組み」です。これを成立させている土台が「レキシカルスコープ(lexical scope)」です。

1-1. レキシカルスコープの最小例

// コード1: レキシカルスコープの最小例
function outer() {
  const message = "hello";
  function inner() {
    console.log(message); // 外側のmessageを参照できる
  }
  inner();
}
outer(); // "hello"

innermessageを引数で受け取っていません。それでも参照できるのは、関数が「定義された場所のスコープ」を覚えているからです。これがレキシカルスコープです。

1-2. クロージャは「関数 + スコープ」のセット

// コード2: クロージャは関数を返した後も生き続ける
function makeGreeter(name) {
  return function () {
    console.log(`Hello, ${name}!`);
  };
}

const greetTaro = makeGreeter("Taro");
const greetHanako = makeGreeter("Hanako");

greetTaro();   // "Hello, Taro!"
greetHanako(); // "Hello, Hanako!"

makeGreeterはすでに実行を終えていますが、返された関数はnameを「閉じ込め(closure)」たまま保持し続けます。これがクロージャです。

1-3. クロージャが「実体」を持つことの確認

// コード3: クロージャごとに別の環境を持つ
function counter() {
  let count = 0;
  return () => ++count;
}

const c1 = counter();
const c2 = counter();

console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 1  ← c1とは独立
console.log(c1()); // 3

c1c2は同じcounterから生まれていますが、それぞれが別のcount変数を抱えています。これがクロージャによる「インスタンスごとの状態」です。

2. 関数スコープ vs ブロックスコープ・var/let/constの挙動差

クロージャを正しく書くには、まず「どの変数がどこまで生きているか」を理解する必要があります。

2-1. var は関数スコープ

// コード4: varは関数スコープ、ブロックを無視する
function varScope() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10 ← ブロックを抜けてもアクセス可能
}
varScope();

2-2. let / const はブロックスコープ

// コード5: let/constはブロックスコープ
function letScope() {
  if (true) {
    let y = 10;
    const z = 20;
  }
  // console.log(y); // ReferenceError
  // console.log(z); // ReferenceError
}
letScope();

2-3. ループ内 setTimeout の有名な罠

// コード6: varを使うと全部 "3" になる罠
for (var i = 0; i  console.log("var i =", i), 10);
}
// 出力: var i = 3 / var i = 3 / var i = 3

varは関数スコープなので、ループが終わったときにはiはすでに3になっており、3つのコールバックは全員その3を見ています。

2-4. let で解決

// コード7: letはイテレーションごとに新しい束縛を作る
for (let i = 0; i  console.log("let i =", i), 10);
}
// 出力: let i = 0 / let i = 1 / let i = 2

2-5. IIFE で var の罠を解決する古典手法

// コード8: IIFE(即時実行関数)でクロージャを作る
for (var i = 0; i  console.log("IIFE j =", j), 10);
  })(i);
}
// 出力: 0, 1, 2

letが使えなかったES5時代の定番テクニックですが、今でもブラウザ環境を厳しく要求される現場や、グローバル汚染を避けたいライブラリ実装で使われます。

3. プライベート変数とカウンター実装

クロージャの最も実用的な使い方の一つが「外から触れない隠し変数を持つ」ことです。

3-1. シンプルなカウンター

// コード9: クロージャで作るプライベートカウンター
function createCounter(initial = 0) {
  let count = initial;
  return {
    inc: () => ++count,
    dec: () => --count,
    get: () => count,
    reset: () => (count = initial),
  };
}

const counter = createCounter(10);
counter.inc();    // 11
counter.inc();    // 12
counter.dec();    // 11
counter.get();    // 11
counter.reset();  // 10
// counter.count は undefined ← 外から触れない

3-2. プライベートメソッド付きの実装

// コード10: プライベートメソッドもクロージャで隠せる
function createBankAccount(owner) {
  let balance = 0;
  const log = (action, amount) =>
    console.log(`[${owner}] ${action}: ${amount}, balance=${balance}`);

  return {
    deposit(amount) {
      balance += amount;
      log("deposit", amount);
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("残高不足");
      balance -= amount;
      log("withdraw", amount);
    },
    getBalance: () => balance,
  };
}

const acc = createBankAccount("Taro");
acc.deposit(1000);
acc.withdraw(300);
acc.getBalance(); // 700

3-3. ID発番器

// コード11: UUIDっぽい連番ID発番器
const createIdGenerator = (prefix = "id") => {
  let n = 0;
  return () => `${prefix}_${++n}_${Date.now().toString(36)}`;
};

const nextUserId = createIdGenerator("user");
nextUserId(); // "user_1_lxxxx"
nextUserId(); // "user_2_lxxxx"

4. メモ化・キャッシュ・デバウンス・スロットル

パフォーマンス改善の定番ユーティリティは、ほぼ全てクロージャで作られています。

4-1. シンプルなメモ化

// コード12: 純粋関数のメモ化(1引数版)
function memoize(fn) {
  const cache = new Map();
  return (arg) => {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

const slowSquare = (n) => {
  console.log("computing", n);
  return n * n;
};
const fastSquare = memoize(slowSquare);
fastSquare(5); // computing 5 → 25
fastSquare(5); // 25 (キャッシュヒット)

4-2. 多引数対応メモ化

// コード13: JSON.stringifyでキーを作る多引数メモ化
function memoizeMulti(fn) {
  const cache = new Map();
  return (...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 add = memoizeMulti((a, b) => {
  console.log("compute");
  return a + b;
});
add(1, 2); // compute → 3
add(1, 2); // 3 (キャッシュ)
add(2, 1); // compute → 3 (引数の順序違いは別キー)

4-3. LRUキャッシュ

// コード14: クロージャで作るLRUキャッシュ(最大件数つき)
function createLRU(max = 100) {
  const cache = new Map();
  return {
    get(key) {
      if (!cache.has(key)) return undefined;
      const v = cache.get(key);
      cache.delete(key);
      cache.set(key, v); // 最新に
      return v;
    },
    set(key, value) {
      if (cache.has(key)) cache.delete(key);
      cache.set(key, value);
      if (cache.size > max) {
        const oldest = cache.keys().next().value;
        cache.delete(oldest);
      }
    },
    size: () => cache.size,
  };
}

const lru = createLRU(3);
lru.set("a", 1); lru.set("b", 2); lru.set("c", 3);
lru.get("a");          // 最新化
lru.set("d", 4);       // b が追い出される
lru.size();            // 3

4-4. デバウンス(debounce)

// コード15: 連続イベントの最後だけ実行するデバウンス
function debounce(fn, wait = 300) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}

const onResize = debounce(() => console.log("resized"), 500);
window.addEventListener("resize", onResize);

4-5. スロットル(throttle)

// コード16: 一定間隔ごとに1回だけ実行するスロットル
function throttle(fn, wait = 200) {
  let last = 0;
  let timer;
  return function (...args) {
    const now = Date.now();
    const remaining = wait - (now - last);
    if (remaining  {
        last = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

const onScroll = throttle(() => console.log("scrolled", Date.now()), 100);
window.addEventListener("scroll", onScroll);

4-6. once(一度だけ実行)

// コード17: 何度呼ばれても初回しか実行しないonce
function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (called) return result;
    called = true;
    result = fn.apply(this, args);
    return result;
  };
}

const init = once(() => {
  console.log("init!");
  return Math.random();
});
init(); // "init!" → 0.xxx
init(); // 同じ 0.xxx(printされない)

5. 関数型プログラミングの基礎パターン

カリー化・partial application・compose/pipeはすべてクロージャの応用です。

5-1. 手動カリー化

// コード18: 引数を1つずつ受け取る関数に変換する手動カリー化
const add3 = (a) => (b) => (c) => a + b + c;
add3(1)(2)(3); // 6

// 部分適用しておくことができる
const add1 = add3(1);
const add1and2 = add1(2);
add1and2(10); // 13

5-2. 汎用 curry 実装

// コード19: 任意引数数の関数をカリー化する汎用curry
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...rest) => curried.apply(this, [...args, ...rest]);
  };
}

const sum4 = (a, b, c, d) => a + b + c + d;
const csum = curry(sum4);
csum(1)(2)(3)(4);    // 10
csum(1, 2)(3, 4);    // 10
csum(1, 2, 3, 4);    // 10

5-3. partial application

// コード20: 先頭引数だけ固定するpartial
function partial(fn, ...preset) {
  return (...rest) => fn(...preset, ...rest);
}

const log = (level, tag, msg) => console.log(`[${level}][${tag}] ${msg}`);
const errorLog = partial(log, "ERROR");
const errorLogApi = partial(log, "ERROR", "API");
errorLog("DB", "connection lost");      // [ERROR][DB] connection lost
errorLogApi("timeout");                 // [ERROR][API] timeout

5-4. compose(右から左へ合成)

// コード21: composeは右から左へ関数を合成する
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);

const trim = (s) => s.trim();
const toLower = (s) => s.toLowerCase();
const exclaim = (s) => `${s}!`;

const shout = compose(exclaim, toLower, trim);
shout("  Hello World  "); // "hello world!"

5-5. pipe(左から右へ合成)

// コード22: pipeは読みやすい順序(左→右)で合成
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const process = pipe(
  (s) => s.trim(),
  (s) => s.toLowerCase(),
  (s) => s.replaceAll(" ", "-"),
  (s) => `slug:${s}`,
);
process("  Hello World  "); // "slug:hello-world"

5-6. Higher-Order Function(高階関数)

// コード23: クロージャで関数を返す = Higher-Order Function
const withLog = (label, fn) => (...args) => {
  console.time(label);
  const result = fn(...args);
  console.timeEnd(label);
  return result;
};

const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
const measuredFib = withLog("fib", fib);
measuredFib(20);

6. モジュールパターンとIIFE

ES Modulesが普及する前、JavaScriptで「名前空間」と「プライベート」を実現する主流テクニックがモジュールパターンでした。今でもライブラリの内部実装ではよく見ます。

6-1. クラシックなモジュールパターン

// コード24: IIFE + クロージャによるモジュールパターン
const CartModule = (function () {
  // private
  const items = [];
  const total = () => items.reduce((s, i) => s + i.price * i.qty, 0);

  // public
  return {
    add(item) { items.push(item); },
    remove(name) {
      const i = items.findIndex((x) => x.name === name);
      if (i >= 0) items.splice(i, 1);
    },
    summary() {
      return { count: items.length, total: total() };
    },
  };
})();

CartModule.add({ name: "Book", price: 1500, qty: 2 });
CartModule.summary(); // { count: 1, total: 3000 }

6-2. リビーリングモジュールパターン

// コード25: 内部で関数を定義してから公開APIだけを露出
const LoggerModule = (function () {
  let level = "INFO";

  function setLevel(v) { level = v; }
  function info(msg) { if (level !== "SILENT") console.log(`[INFO] ${msg}`); }
  function error(msg) { console.error(`[ERROR] ${msg}`); }
  function _format(msg) { return `[${new Date().toISOString()}] ${msg}`; }

  return {
    setLevel,
    info: (m) => info(_format(m)),
    error: (m) => error(_format(m)),
  };
})();

LoggerModule.info("hello");
LoggerModule.error("boom");

6-3. シングルトンの実装

// コード26: クロージャで強制シングルトン
const ConfigSingleton = (() => {
  let instance = null;
  return {
    get() {
      if (!instance) {
        instance = { apiUrl: "https://api.example.com", debug: false };
      }
      return instance;
    },
  };
})();

const a = ConfigSingleton.get();
const b = ConfigSingleton.get();
console.log(a === b); // true

7. React・Reduxの内部実装をクロージャで再現する

クロージャを理解する最大のリターンは「ReactのuseStateやReduxの内部実装が腑に落ちる」ことです。

7-1. useState のミニ実装

// コード27: クロージャで作るuseStateの最小版
function createUseState() {
  let state;
  let initialized = false;
  return function useState(initial) {
    if (!initialized) {
      state = typeof initial === "function" ? initial() : initial;
      initialized = true;
    }
    const setState = (next) => {
      state = typeof next === "function" ? next(state) : next;
      console.log("re-render with", state);
    };
    return [state, setState];
  };
}

const useState = createUseState();
const [count, setCount] = useState(0);
setCount(1);              // re-render with 1
setCount((c) => c + 10);  // re-render with 11

7-2. 複数useStateを扱う配列ベース実装

// コード28: Reactが内部でやっているフックの順序保証
function createHookSystem() {
  let states = [];
  let index = 0;

  function useState(initial) {
    const i = index;
    if (states[i] === undefined) {
      states[i] = typeof initial === "function" ? initial() : initial;
    }
    const setState = (next) => {
      states[i] = typeof next === "function" ? next(states[i]) : next;
    };
    index++;
    return [states[i], setState];
  }

  function render(component) {
    index = 0;
    component();
  }

  return { useState, render };
}

const { useState, render } = createHookSystem();
function Counter() {
  const [n, setN] = useState(0);
  const [name, setName] = useState("Taro");
  console.log(n, name);
  if (n < 2) setN(n + 1);
}
render(Counter);
render(Counter);
render(Counter);

7-3. createStore(Reduxの本体)

// コード29: Reduxのcreate Storeはほぼ100%クロージャ
function createStore(reducer, initial) {
  let state = initial;
  const listeners = new Set();

  return {
    getState: () => state,
    dispatch(action) {
      state = reducer(state, action);
      listeners.forEach((l) => l(state));
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

const counterReducer = (s = 0, a) => {
  switch (a.type) {
    case "INC": return s + 1;
    case "DEC": return s - 1;
    default: return s;
  }
};

const store = createStore(counterReducer, 0);
const unsubscribe = store.subscribe((s) => console.log("state", s));
store.dispatch({ type: "INC" }); // state 1
store.dispatch({ type: "INC" }); // state 2
unsubscribe();

7-4. カスタムフック風に状態を保持

// コード30: クロージャだけで作るシンプルなObservable
function createSignal(initial) {
  let value = initial;
  const subs = new Set();
  return {
    get: () => value,
    set(v) {
      value = v;
      subs.forEach((fn) => fn(value));
    },
    subscribe(fn) {
      subs.add(fn);
      return () => subs.delete(fn);
    },
  };
}

const count = createSignal(0);
count.subscribe((v) => console.log("count =", v));
count.set(1); // count = 1
count.set(2); // count = 2

7-5. useRef のミニ実装

// コード31: useRefもクロージャで保持される単一オブジェクト
function createUseRef() {
  let ref;
  return (initial) => {
    if (!ref) ref = { current: initial };
    return ref;
  };
}

const useRef = createUseRef();
const r1 = useRef(0);
r1.current = 5;
const r2 = useRef(999);
console.log(r2.current); // 5 ← 同一オブジェクトを返す

8. クロージャと this、async、TypeScript

8-1. クロージャと this(アロー関数の活用)

// コード32: アロー関数は自分のthisを持たず、外側のthisを「閉じ込める」
const obj = {
  name: "Taro",
  greetLater() {
    setTimeout(function () {
      console.log("classic this:", this?.name); // undefined
    }, 0);
    setTimeout(() => {
      console.log("arrow this:", this.name); // "Taro"
    }, 0);
  },
};
obj.greetLater();

8-2. bindとクロージャの違い

// コード33: bindはクロージャ的にthisを固定する
function describe() {
  return `I am ${this.name}`;
}
const taro = { name: "Taro" };
const describeAsTaro = describe.bind(taro);
describeAsTaro(); // "I am Taro"

8-3. クロージャ × async

// コード34: async関数の中でもクロージャは健在
function createApiClient(baseUrl, token) {
  return {
    async get(path) {
      const res = await fetch(`${baseUrl}${path}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      return res.json();
    },
    async post(path, body) {
      const res = await fetch(`${baseUrl}${path}`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
      });
      return res.json();
    },
  };
}

const api = createApiClient("https://api.example.com", "xxx");
// api.get("/users") のたびにbaseUrl/tokenがクロージャから渡される

8-4. リトライ付き fetch

// コード35: クロージャで状態(試行回数)を保持するリトライfetch
function createRetryFetch(maxRetry = 3, delay = 500) {
  return async function retry(url, options = {}) {
    let attempt = 0;
    while (true) {
      try {
        const res = await fetch(url, options);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res;
      } catch (e) {
        attempt++;
        if (attempt > maxRetry) throw e;
        await new Promise((r) => setTimeout(r, delay * attempt));
      }
    }
  };
}

const safeFetch = createRetryFetch(3, 300);
// safeFetch("https://example.com/api") で自動リトライ

8-5. TypeScript でのクロージャ型付け

// コード36: クロージャの戻り型をTypeScriptで明示する
type Counter = {
  inc: () => number;
  get: () => number;
};

function createCounter(initial = 0): Counter {
  let count = initial;
  return {
    inc: () => ++count,
    get: () => count,
  };
}

const c: Counter = createCounter();
c.inc(); // 1

8-6. ジェネリックなメモ化

// コード37: 任意関数を型安全にメモ化
function memoize(
  fn: (...args: TArgs) => TResult,
): (...args: TArgs) => TResult {
  const cache = new Map();
  return (...args: TArgs): TResult => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const slow = (a: number, b: number): number => a + b;
const fast = memoize(slow);
fast(1, 2); // 3

9. メモリリーク・GC・アンチパターン

クロージャは強力ですが、扱いを間違えるとメモリリークやパフォーマンス劣化の温床になります。

9-1. クロージャがGCを止める例

// コード38: 巨大なオブジェクトをクロージャに閉じ込めて捨て忘れる
function leak() {
  const huge = new Array(1_000_000).fill("x"); // 数MB
  return () => console.log("hi"); // hugeを使わないのに参照だけ残る
}
const fn = leak();
// fn が生きている間、huge も解放されない

対策:必要のないキャプチャは関数の外でnullを代入して切るか、そもそもキャプチャしないように設計する。

9-2. リーク対策版

// コード39: 不要参照を断ち切ってGCを許可する
function noLeak() {
  let huge = new Array(1_000_000).fill("x");
  const usefulLen = huge.length;
  huge = null; // ← 明示的に解放
  return () => console.log("length was", usefulLen);
}
const fn2 = noLeak();

9-3. addEventListener の解除忘れ

// コード40: イベントリスナーで参照が残り続けるリーク
function attach(element) {
  const big = new Array(100_000).fill("data");
  const handler = () => console.log(big.length);
  element.addEventListener("click", handler);
  // ❌ 解除関数を返さないとbigが解放されない
  return () => element.removeEventListener("click", handler);
}
const detach = attach(document.body);
// 使い終わったら detach() を必ず呼ぶ

9-4. setIntervalのリーク

// コード41: setIntervalはクリアしないと永遠にクロージャが生き続ける
function startPoller(url) {
  const cache = new Map();
  const id = setInterval(async () => {
    const r = await fetch(url);
    cache.set(Date.now(), await r.json());
  }, 5000);
  return () => {
    clearInterval(id);  // ← これを忘れるとcacheも残る
    cache.clear();
  };
}
const stop = startPoller("/api/health");
// stop() を呼ぶまでメモリは増え続ける

9-5. アンチパターン: クロージャの乱用

// コード42: クラス的な使い方ならclassの方が読みやすいこともある
// ❌ 過剰にクロージャに頼った例
function createUser(name) {
  let age = 0;
  return {
    setAge: (a) => (age = a),
    getAge: () => age,
    setName: (n) => (name = n),
    getName: () => name,
    introduce: () => `${name} (${age})`,
  };
}

// ✅ 状態が多いならclassの方が型推論もデバッグも楽
class User {
  #age = 0;
  constructor(public name) {}
  setAge(a) { this.#age = a; }
  getAge() { return this.#age; }
  introduce() { return `${this.name} (${this.#age})`; }
}

private fields(#xxx)が普及した今、「プライベートを作るためだけのクロージャ」はclass + privateで代替可能です。ただし、関数1個だけで完結する小さい状態(カウンター、once、debounceなど)はクロージャの方が圧倒的に簡潔です。

9-6. パフォーマンス: 高頻度呼び出しでクロージャを毎回作らない

// コード43: ❌ 毎回新しいクロージャを作っている
items.forEach((item) => {
  button.addEventListener("click", () => handle(item));
});

// ✅ ハンドラを1つにしてdata属性で識別する
button.addEventListener("click", (e) => {
  const id = e.target.dataset.id;
  handle(items.find((i) => i.id === id));
});

9-7. WeakMapで参照を弱く保つ

// コード44: WeakMapならキーが消えれば値も自動GC
function createPrivateStorage() {
  const store = new WeakMap();
  return {
    set(obj, key, value) {
      if (!store.has(obj)) store.set(obj, {});
      store.get(obj)[key] = value;
    },
    get(obj, key) {
      return store.get(obj)?.[key];
    },
  };
}

const priv = createPrivateStorage();
let user = { name: "Taro" };
priv.set(user, "secret", "xxx");
priv.get(user, "secret"); // "xxx"
user = null; // userが消えれば、priv内のデータも自動的に解放される

10. 実用パターン集とまとめ

10-1. リソースプール

// コード45: クロージャで作る簡易プール
function createPool(factory, max = 5) {
  const pool = [];
  let active = 0;
  return {
    async acquire() {
      if (pool.length) return pool.pop();
      if (active < max) {
        active++;
        return await factory();
      }
      // 簡略化: 本番はwaitキューを実装
      throw new Error("pool exhausted");
    },
    release(item) {
      pool.push(item);
    },
  };
}

10-2. パイプライン処理

// コード46: ミドルウェアチェーン(Expressライク)
function createPipeline() {
  const middlewares = [];
  return {
    use(fn) { middlewares.push(fn); return this; },
    async run(ctx) {
      let i = 0;
      const next = async () => {
        const m = middlewares[i++];
        if (m) await m(ctx, next);
      };
      await next();
      return ctx;
    },
  };
}

const p = createPipeline()
  .use(async (ctx, next) => { console.log("A in"); await next(); console.log("A out"); })
  .use(async (ctx, next) => { console.log("B in"); await next(); console.log("B out"); });
p.run({});
// A in / B in / B out / A out

10-3. イベントエミッタ

// コード47: クロージャで作る軽量EventEmitter
function createEmitter() {
  const listeners = new Map();
  return {
    on(event, fn) {
      if (!listeners.has(event)) listeners.set(event, new Set());
      listeners.get(event).add(fn);
      return () => listeners.get(event).delete(fn);
    },
    emit(event, ...args) {
      listeners.get(event)?.forEach((fn) => fn(...args));
    },
  };
}

const ee = createEmitter();
const off = ee.on("hello", (name) => console.log("hi", name));
ee.emit("hello", "Taro"); // hi Taro
off();

10-4. 状態機械(State Machine)

// コード48: 簡易ステートマシン
function createStateMachine(initial, transitions) {
  let state = initial;
  const subs = new Set();
  return {
    getState: () => state,
    send(event) {
      const next = transitions[state]?.[event];
      if (next) {
        state = next;
        subs.forEach((s) => s(state));
      }
    },
    subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
  };
}

const traffic = createStateMachine("red", {
  red:    { TICK: "green" },
  green:  { TICK: "yellow" },
  yellow: { TICK: "red" },
});
traffic.subscribe((s) => console.log("light:", s));
traffic.send("TICK"); // green
traffic.send("TICK"); // yellow
traffic.send("TICK"); // red

10-5. レート制限(Rate Limiter)

// コード49: トークンバケット風レートリミッタ
function createRateLimiter(maxPerSec = 5) {
  let tokens = maxPerSec;
  setInterval(() => (tokens = maxPerSec), 1000);
  return async function limited(fn) {
    while (tokens  setTimeout(r, 50));
    }
    tokens--;
    return fn();
  };
}

10-6. 関数キャンセル(AbortController風)

// コード50: クロージャでキャンセル可能な処理を作る
function createCancelable(asyncFn) {
  let canceled = false;
  const promise = (async () => {
    const result = await asyncFn();
    if (canceled) throw new Error("canceled");
    return result;
  })();
  return {
    promise,
    cancel: () => { canceled = true; },
  };
}

const task = createCancelable(async () => {
  await new Promise((r) => setTimeout(r, 1000));
  return "done";
});
// 必要に応じて task.cancel();
task.promise.then(console.log).catch((e) => console.log("err:", e.message));

11. クロージャを学んだ後のキャリア戦略

クロージャ・関数型JS・React/Reduxの内部構造まで理解できれば、Webフロントエンドの設計レビューで「この実装はなぜダメか」を言語化できる側に立てます。ここまで来たら、次の一手は「ポートフォリオに反映」「実務経験を積める環境への移動」です。

  • テックアカデミー: 短期集中でフロントエンド・React・Node.jsを実務レベルまで引き上げたい人向け。現役エンジニアによるメンタリングが強み。
  • 侍エンジニア: マンツーマンでオリジナルプロダクト開発まで伴走。クロージャ・関数型・状態管理の設計まで個別に詰めたい人向け。
  • DMM WEBCAMP: 転職保証付きでWeb系企業を狙う人向け。フロントエンドの基礎から実務直結スキルまで一気通貫。
  • レバテック: 実務経験がすでにあるエンジニアが、React/TypeScript/Next.jsの高単価フロント案件を探すならまずここ。

12. まとめ

本記事では、JavaScriptのクロージャを30以上のコピペで動くコードで体系的に解説しました。重要ポイントを最後に整理します。

  • クロージャ = 関数 + 定義時のスコープ。関数を返した後もスコープは生き続ける
  • var/let/constの違いを理解しないと、ループ内setTimeoutで詰む
  • プライベート変数・カウンター・メモ化・debounce/throttle・onceは全部クロージャで作れる
  • カリー化・compose/pipe・高階関数は関数型JSの基礎
  • モジュールパターン・IIFEはES Modules前夜の定番、今もライブラリ実装で生きている
  • useState・Reduxストア・useRefはすべてクロージャで再現できる
  • メモリリークに注意: addEventListener / setInterval / 巨大配列のキャプチャは要解放
  • WeakMap・class + private fieldと使い分けることで保守性が上がる

クロージャを「言葉」ではなく「コードを書いて動かせる」ところまで持っていけたら、もうJavaScriptの中級者ラインは超えています。次のステップとして、Iterator/Generatorエラー処理と組み合わせれば、現場で求められる関数型・非同期・状態管理を一気通貫で書けるようになります。

コメント

タイトルとURLをコピーしました