JavaScriptを学んでいて「クロージャ(closure)」という単語にぶつかった瞬間、急に話が抽象的になって挫折した人は多いはずです。しかし実務では、useStateの内部実装、デバウンス・スロットル、メモ化キャッシュ、モジュールパターン、カリー化、Reactのカスタムフック、Reduxのreducer合成など、クロージャを理解していないと書けないコードだらけです。
この記事では「クロージャとは何か」を曖昧な比喩ではなく、30個以上のコピペで動くES2025準拠コードで徹底的に体感してもらいます。レキシカルスコープの根本から、プライベート変数、関数型のcompose/pipe、Reactの内部実装、TypeScriptでの型付け、メモリリークの罠まで、現場で必要な「クロージャの全部」を1記事で押さえます。
1. クロージャの定義とレキシカルスコープ
クロージャを一言で言えば「関数が、自分の外側のスコープにある変数を、関数が呼ばれるたびに参照し続ける仕組み」です。これを成立させている土台が「レキシカルスコープ(lexical scope)」です。
1-1. レキシカルスコープの最小例
// コード1: レキシカルスコープの最小例
function outer() {
const message = "hello";
function inner() {
console.log(message); // 外側のmessageを参照できる
}
inner();
}
outer(); // "hello"
innerはmessageを引数で受け取っていません。それでも参照できるのは、関数が「定義された場所のスコープ」を覚えているからです。これがレキシカルスコープです。
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
c1とc2は同じ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やエラー処理と組み合わせれば、現場で求められる関数型・非同期・状態管理を一気通貫で書けるようになります。

コメント