クロージャとは、関数が「自分が定義された場所の変数」を覚えていて、その場所の外で呼ばれても使い続けられる仕組みです。 JavaScript ではすべての関数がこの性質を持っています。特別な構文があるわけではなく、「関数の中で関数を作り、外側の変数を使う」だけでクロージャになります。

この記事では、使いどころを7つ(カウンター、プライベート変数、関数ファクトリ、debounce/throttle、メモ化、once、イベントハンドラ)に分け、コピペできるコードと Node.js 22.22.2 での実行結果を並べました。さらに、for と var で全部同じ値になる現象、React で更新前の state を掴んでしまう stale closure、参照が残り続けるメモリリークを実際に再現し、直し方まで載せています。スコープチェーンの図で「なぜ変数が生き残るのか」も一段深く説明します。

クロージャの使いどころ早見表

使いどころ 覚えておく変数 得られること 最小の形
1. カウンター 現在の回数 呼ぶたびに状態が進む。グローバル変数が不要 let count = 0; return () => ++count;
2. プライベート変数 外から触らせたくない値 決めた操作だけで値を変えられる return { add() {...}, get value() {...} }
3. 関数ファクトリ 設定値(倍率・接頭辞など) 似た関数を引数1つで量産できる rate => value => value * rate
4. debounce / throttle タイマーID・前回の時刻 連続する呼び出しを間引ける let timerId; return (...a) => {...}
5. メモ化 計算結果のキャッシュ 同じ引数の再計算を省ける const cache = new Map(); return arg => {...}
6. once 実行済みフラグと結果 初期化を1回だけにできる let called = false; return () => {...}
7. イベントハンドラ 要素ごとの状態・解除用の関数 登録と解除を対で扱える return () => el.removeEventListener(...)

つまずきやすい3つの現象と直し方です。

現象 起きること 直し方
for + var の罠 全部の関数が同じ値([3, 3, 3])を返す let にする
stale closure(React) カウンターが 1 で止まる setCount(c => c + 1)、依存配列、useRef
メモリリーク 不要になった大きなデータが解放されない 必要な値だけ取り出す/登録を解除する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript クロージャとは?使いどころ7例をコード付きで図解」

クロージャとは:最小の例で確かめる

function createCounter() {
  let count = 0;            // createCounter の中の変数
  return function () {      // 内側の関数を返す
    count += 1;             // 外側の変数を使っている
    return count;
  };
}

const counterA = createCounter();
console.log(counterA(), counterA(), counterA());
console.log(typeof count);

実行結果:

1 2 3
undefined

注目したい点は2つです。

  • createCounter() の実行はもう終わっているのに、count が消えずに 1 → 2 → 3 と増えている
  • 外から count を見ても undefined。count に触れられるのは、返された関数だけ

この「内側の関数」と「その関数が覚えている外側の変数たち」の組み合わせがクロージャです。

用語 意味
スコープ 変数を参照できる範囲。関数や {} ブロックごとに作られる
レキシカルスコープ 変数の参照先が「コードを書いた位置」で決まるというルール
スコープチェーン 内側から外側へ順に変数を探していくつながり
クロージャ 関数と、その関数が定義された場所のスコープの組み合わせ

なぜ変数が生き残るのか:スコープチェーンの図解

変数は内側から外側へ探される

関数の中で変数名が出てくると、JavaScript はまず自分のスコープ、なければ1つ外側、さらに外側…という順で探します。このつながりがスコープチェーンです。

const globalName = 'G';

function outer() {
  const a = 'outer-a';
  function middle() {
    const b = 'middle-b';
    return function inner() {
      const c = 'inner-c';
      return [c, b, a, globalName].join(' > ');
    };
  }
  return middle();
}

console.log(outer()());

実行結果:

inner-c > middle-b > outer-a > G
flowchart LR
    I["inner のスコープ<br>c = 'inner-c'"] -->|なければ外へ| M["middle のスコープ<br>b = 'middle-b'"]
    M -->|なければ外へ| O["outer のスコープ<br>a = 'outer-a'"]
    O -->|なければ外へ| G["モジュール(グローバル)<br>globalName = 'G'"]

どこを探すかは、関数を「呼んだ場所」ではなく「書いた場所」で決まります。 これがレキシカルスコープです。

関数は「生まれた場所への参照」を持ち歩く

関数が作られるとき、その関数には「自分が作られたスコープへの参照」がこっそり付けられます(仕様上は [[Environment]] という内部スロットです)。関数を変数に入れても、別の関数に渡しても、この参照は付いたままです。

flowchart TD
    subgraph call1["createCounter() 1回目の呼び出しで作られたスコープ"]
        C1["count = 3"]
    end
    subgraph call2["createCounter() 2回目の呼び出しで作られたスコープ"]
        C2["count = 1"]
    end
    A["counterA(関数)"] -->|作られた場所への参照| call1
    B["counterB(関数)"] -->|作られた場所への参照| call2

通常、関数の実行が終わると、その中の変数は誰からも参照されなくなり、ガベージコレクション(不要になったメモリを自動で回収する仕組み)の対象になります。ところがクロージャの場合、返された関数がスコープを参照し続けているので、回収されません。これが「変数が生き残る」理由です。

逆に言えば、返された関数(上の counterA)をどこからも参照しなくなれば、スコープごと回収されます。後半のメモリリークの節で、実際に数値で確かめます。

呼び出しごとに別のスコープができる

const counterA = createCounter();
const counterB = createCounter();
console.log(counterA(), counterA(), counterA()); // 1 2 3
console.log(counterB());                         // 1

createCounter() を呼ぶたびに新しいスコープが作られるので、counterA と counterB は別々の count を持ちます。同じ関数から作っても、状態は混ざりません。

覚えているのは「値のコピー」ではなく「変数そのもの」

function live() {
  let v = 1;
  const get = () => v;
  v = 2;            // get を作ったあとで書き換える
  return get;
}
console.log(live()()); // 2

get を作った時点の 1 ではなく、2 が返ります。クロージャは変数の値を写し取るのではなく、変数そのものを参照しています。 この性質は、複数の関数で1つの状態を共有できる利点になる一方、後述の「for + var の罠」の原因にもなります。

function pair() {
  let n = 0;
  return { inc: () => ++n, get: () => n };
}
const p = pair();
p.inc(); p.inc();
console.log(p.get()); // 2(inc と get が同じ n を見ている)

使いどころ1:カウンター(状態を持つ関数)

最初の例がそのまま使いどころの1つ目です。連番の発行に応用した形を載せます。

function createIdGenerator(prefix, start = 1) {
  let next = start;
  return () => `${prefix}-${String(next++).padStart(4, '0')}`;
}

const nextOrderId = createIdGenerator('ORD');
const nextMemberId = createIdGenerator('MEM', 100);

console.log(nextOrderId(), nextOrderId());
console.log(nextMemberId());

実行結果:

ORD-0001 ORD-0002
MEM-0100

グローバル変数に let orderSeq = 1; と置く方法でも同じことはできますが、どこからでも書き換えられてしまいます。クロージャなら、番号を進められるのは返された関数だけです。

使いどころ2:プライベート変数(カプセル化)

外から直接書き換えさせたくない値を、決めた操作だけで扱えるようにします。

function createStock(initial) {
  let quantity = initial;   // 外からは見えない

  return {
    add(n) {
      quantity += n;
      return quantity;
    },
    remove(n) {
      if (n > quantity) {
        throw new RangeError(`在庫が足りません(残り ${quantity})`);
      }
      quantity -= n;
      return quantity;
    },
    get quantity() {
      return quantity;
    },
  };
}

const stock = createStock(10);
console.log(stock.add(5), stock.remove(3), stock.quantity);

try { stock.quantity = 999; } catch (e) { console.log(`${e.name}: ${e.message}`); }
console.log(stock.quantity, Object.keys(stock));

try { stock.remove(100); } catch (e) { console.log(`${e.name}: ${e.message}`); }

実行結果(ES モジュールとして実行):

15 12 12
TypeError: Cannot set property quantity of #<Object> which has only a getter
12 [ 'add', 'remove', 'quantity' ]
RangeError: 在庫が足りません(残り 12)

stock.quantity = 999 は受け付けられず、値は 12 のままです(strict モードでは TypeError、非 strict モードでは何も起きずに無視されます)。在庫数を変える手段が add と remove だけになるので、「マイナスにならない」というルールを1か所で守れます。

同じ目的は class の # 付きプライベートフィールドでも実現できます。

方法 書き方 向いている場面
クロージャ function createStock() { let quantity; return {...}; } 小さな部品、関数だけ返したい、this を気にしたくない
class の #field class Stock { #quantity; ... } インスタンスが多い、継承や instanceof を使いたい

使いどころ3:関数ファクトリ(設定を覚えた関数を作る)

引数で受け取った設定を覚えた関数を返す形です。似た関数をいくつも書く代わりに、1つの「関数を作る関数」で済みます。

// 倍率を覚える
const multiplyBy = rate => value => value * rate;
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5), triple(5));

// 接頭辞を覚える
const withPrefix = prefix => message => `[${prefix}] ${message}`;
const info = withPrefix('INFO');
const warn = withPrefix('WARN');
console.log(info('起動しました'));
console.log(warn('残り容量が少なくなっています'));

// 並べ替えのキーを覚える
const byKey = key => (a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0);
const items = [{ n: 'b', p: 2 }, { n: 'a', p: 3 }];
console.log(items.toSorted(byKey('n')).map(x => x.n));

実行結果:

10 15
[INFO] 起動しました
[WARN] 残り容量が少なくなっています
[ 'a', 'b' ]

array.sort(byKey('n')) や array.filter(isOver(100)) のように、配列メソッドへ渡す関数を「その場で設定して作る」ときに特に役立ちます。

使いどころ4:debounceとthrottle(呼び出しを間引く)

入力やスクロールのように短時間に何度も起きる処理を間引く関数です。どちらも「タイマーID」や「前回実行した時刻」をクロージャに覚えさせることで成り立っています。

// 最後の呼び出しから wait ミリ秒たったら1回だけ実行
function debounce(fn, wait) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn.apply(this, args), wait);
  };
}

// interval ミリ秒に1回だけ実行
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

const sleep = ms => new Promise(r => setTimeout(r, ms));
const dlog = [], tlog = [];
const search = debounce(word => dlog.push(word), 100);
const onScroll = throttle(y => tlog.push(y), 100);

for (const w of ['j', 'ja', 'jav', 'java']) { search(w); await sleep(30); }
for (let y = 0; y < 10; y++) { onScroll(y * 10); await sleep(30); }
await sleep(150);

console.log('debounce:', dlog);
console.log('throttle:', tlog.length, '回 /10回中');

実行結果:

debounce: [ 'java' ]
throttle: 3 回 /10回中

30ミリ秒おきに4回呼んだ search は最後の 'java' だけ、10回呼んだ onScroll は3回だけ実行されました。

関数 覚えている変数 動き 向いている処理
debounce timerId 呼び出しが止まってから1回 検索ボックスの入力、ウィンドウのリサイズ後の再計算
throttle last 一定間隔ごとに1回 スクロール位置の記録、連打されるボタン

timerId が関数の外(グローバル)にあると、複数の debounce 関数が1つのタイマーを取り合ってしまいます。クロージャに入れておくことで、作った関数ごとに独立したタイマーを持てます。 キャンセル機能や最後の1回を必ず実行する版などの発展形は、debounce・throttle を扱う別の記事で詳しく取り上げています。

使いどころ5:メモ化(計算結果を覚えておく)

同じ引数で何度も呼ばれる重い計算を、結果をキャッシュして2回目以降は即座に返すようにします。

function memoize(fn) {
  const cache = new Map();          // クロージャが覚えておくキャッシュ
  return function (arg) {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

let calls = 0;
const slowSquare = n => { calls += 1; return n * n; };
const fastSquare = memoize(slowSquare);
console.log(fastSquare(12), fastSquare(12), fastSquare(12), 'calls =', calls);

// 再帰と組み合わせる
const plainFib = n => (n < 2 ? n : plainFib(n - 1) + plainFib(n - 2));
const fib = memoize(n => (n < 2 ? n : fib(n - 1) + fib(n - 2)));

let t = performance.now();
const r1 = plainFib(32);
const t1 = performance.now() - t;

t = performance.now();
const r2 = fib(32);
const t2 = performance.now() - t;

console.log(r1, r2, `plain ${t1.toFixed(1)}ms / memo ${t2.toFixed(3)}ms`);

実行結果(時間は環境により変わります):

144 144 144 calls = 1
2178309 2178309 plain 24.8ms / memo 0.100ms

3回呼んでも元の関数が動いたのは1回だけです。フィボナッチ数の計算では、手元の環境で約24.8ミリ秒が約0.1ミリ秒になりました。

メモ化を使うときの注意点は次のとおりです。

確認すること 理由
同じ引数なら必ず同じ結果になる関数か 現在時刻や外部の状態に左右される関数は、キャッシュした値とずれる
引数がオブジェクトではないか Map のキーは参照で比べるので、中身が同じでも別のオブジェクトは別扱いになる
キャッシュが増え続けないか 引数の種類が多いと cache が大きくなる。上限を設けるか、オブジェクトがキーなら WeakMap を使う

使いどころ6:once(1回だけ実行する)

初期化や設定の読み込みなど、何度呼ばれても最初の1回だけ実行したい処理に使います。

function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

const init = once(() => {
  console.log('初期化しました');
  return { ready: true };
});

console.log(init(), init(), init() === init());

実行結果:

初期化しました
{ ready: true } { ready: true } true

「初期化しました」は1回だけ表示され、2回目以降は最初の結果と同じオブジェクトが返ります。called と result はクロージャの中にあるので、外から「未実行」に戻されることがありません。

なお、イベントリスナーを1回だけ動かしたい場合は、addEventListener('click', handler, { once: true }) というオプションが用意されています。

使いどころ7:イベントハンドラ(要素ごとの状態と解除)

イベントハンドラは、登録してから実際に呼ばれるまで時間が空くので、クロージャが自然に登場します。要素ごとの状態を覚えておき、解除用の関数も返す形にすると扱いやすくなります。

function attachClickCounter(target, label) {
  let clicks = 0;
  const handler = () => {
    clicks += 1;
    console.log(`${label}: ${clicks}回目`);
  };
  target.addEventListener('click', handler);

  // 解除用の関数を返す(handler を覚えているクロージャ)
  return () => target.removeEventListener('click', handler);
}

// Node.js でも EventTarget で同じ動きを確かめられる
const target = new EventTarget();
const detach = attachClickCounter(target, '保存ボタン');

target.dispatchEvent(new Event('click'));
target.dispatchEvent(new Event('click'));
detach();
target.dispatchEvent(new Event('click'));
console.log('解除後は出力なし');

実行結果:

保存ボタン: 1回目
保存ボタン: 2回目
解除後は出力なし

ブラウザでは target を document.querySelector('#save') などの要素に置き換えるだけです。

removeEventListener には登録したときと同じ関数を渡す必要があります。解除用の関数が handler を覚えていてくれるので、呼び出す側は detach() と書くだけで済みます。React の useEffect が返すクリーンアップ関数も、これと同じ形です。

つまずき1:for+varで全部同じ値になる

ここからは、クロージャでつまずきやすい現象を3つ再現します。1つ目はループです。

const fv = [];
for (var i = 0; i < 3; i++) {
  fv.push(() => i);
}
console.log('var:', fv.map(f => f()));

const fl = [];
for (let j = 0; j < 3; j++) {
  fl.push(() => j);
}
console.log('let:', fl.map(f => f()));

実行結果:

var: [ 3, 3, 3 ]
let: [ 0, 1, 2 ]

setTimeout と組み合わせても同じ結果になります(var では [ 3, 3, 3 ]、let では [ 0, 1, 2 ])。

原因は「クロージャは値のコピーではなく変数そのものを参照する」という性質です。

flowchart TD
    subgraph V["var の場合:変数 i は1つだけ"]
        VI["i = 3(ループ終了後の値)"]
        F0["関数0"] --> VI
        F1["関数1"] --> VI
        F2["関数2"] --> VI
    end
    subgraph L["let の場合:周回ごとに新しい j"]
        G0["関数0"] --> J0["j = 0"]
        G1["関数1"] --> J1["j = 1"]
        G2["関数2"] --> J2["j = 2"]
    end
宣言 スコープ ループでの動き 結果
var i 関数(またはファイル)全体で1つ 3つの関数が同じ i を参照。呼ばれた時点で i は 3 [3, 3, 3]
let j ブロック(周回)ごと 周回のたびに新しい j が作られる [0, 1, 2]

直し方は3通りあります。

// 1. let にする(基本はこれ)
for (let i = 0; i < 3; i++) { fns.push(() => i); }

// 2. 配列メソッドを使う(コールバックの引数は呼び出しごとに別)
const fns2 = [0, 1, 2].map(n => () => n);

// 3. 即時実行関数で周回ごとのスコープを作る(let がなかった頃からある書き方)
for (var k = 0; k < 3; k++) {
  (function (n) { fns3.push(() => n); })(k);
}

3つとも [ 0, 1, 2 ] になることを確認しています。今は let(または const と for...of)を使えば済みます。

つまずき2:Reactで更新前のstateを掴むstale closure

stale closure とは、クロージャが覚えている変数が、すでに最新ではなくなっている状態のことです。React の関数コンポーネントでよく出会います。

再現:カウンターが1で止まる

import { useState, useEffect } from 'react';

function StaleCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);      // この count は最初の描画のときの 0 のまま
    }, 50);
    return () => clearInterval(id);
  }, []);                        // 依存配列が空 = 最初の1回だけ実行

  return <p>count: {count}</p>;
}

React 19.3.0 で、JSX を createElement に置き換えたものを Node.js + jsdom 上で約0.33秒動かした結果です。

StaleCounter : count: 1

50ミリ秒ごとに6回ほど setCount が呼ばれているのに、1 から進みません。

なぜ起きるか

React の関数コンポーネントは、描画のたびに関数全体がもう一度呼ばれます。つまり count は描画ごとに別の変数です。

flowchart LR
    subgraph R1["1回目の描画のスコープ"]
        C0["count = 0"]
    end
    subgraph R2["2回目の描画のスコープ"]
        C1["count = 1"]
    end
    T["setInterval のコールバック<br>(1回目の描画で作られた)"] --> C0
    T -. 見えていない .-> C1

依存配列が [] なので、useEffect の中身は1回目の描画のときにだけ実行されます。そこで作られたコールバックは 1回目の描画の count(=0)を覚えたままです。何度呼ばれても setCount(0 + 1) を繰り返すので、1 で止まります。

直し方3通り

// 1. 関数型の更新:最新の値を引数で受け取る
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 50);
  return () => clearInterval(id);
}, []);

// 2. 依存配列に入れる:count が変わるたびにタイマーを作り直す
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 50);
  return () => clearInterval(id);
}, [count]);

// 3. useRef に最新の値を入れておく:読むだけの用途に
const latest = useRef(count);
useEffect(() => { latest.current = count; });

同じ条件で動かした結果です(回数はタイマーの誤差で前後します)。

FixedUpdater : count: 7
FixedDeps    : count: 6
FixedRef     : stateの変数=0 / ref=5

3つ目は、280ミリ秒後に「最初の描画で掴んだ count」と「latest.current」を読み比べたものです。変数のほうは 0 のまま、ref は 5 と最新に近い値を返しました。

直し方 向いている場面 補足
setCount(c => c + 1) 前の値をもとに更新したい タイマーを作り直さずに済む。まずこれを検討
依存配列に入れる 値が変わったら処理をやり直したい 変わるたびにクリーンアップと再実行が走る
useRef コールバックの中で最新の値を「読む」だけ ref の変更では再描画されない

依存配列の書き漏れは、ESLint の react-hooks/exhaustive-deps ルールで検出できます(この記事では未実行。ルール名は一般に知られた仕様として記載しています)。

つまずき3:参照が残り続けるメモリリーク

クロージャが変数を生かし続ける性質は、裏を返すと「クロージャが生きている限り、変数も解放されない」ということです。Node.js 22.22.2 で node --expose-gc leak.mjs として実行し、ヒープ使用量を測りました。

再現:大きな配列を掴んだままになる

// node --expose-gc leak.mjs
const mb = () => (gc(), (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB');
console.log('開始時:', mb());

// A: クロージャが大きな配列を参照し続ける
const handlersA = [];
function registerA() {
  const rows = new Array(1_000_000).fill('row');   // 約 8 MB
  handlersA.push(() => rows.length);
}
for (let i = 0; i < 10; i++) registerA();
console.log('A 登録後:', mb());

handlersA.length = 0;                               // クロージャへの参照をなくす
console.log('A 解除後:', mb());

実行結果:

開始時: 3.5 MB
A 登録後: 80.0 MB
A 解除後: 3.7 MB

必要なのは件数(rows.length)だけなのに、クロージャが rows 全体を参照しているため、約76 MB が保持されたままになりました。クロージャへの参照をなくすと、3.7 MB に戻ります。

直し方1:必要な値だけ取り出してからクロージャを作る

const handlersB = [];
function registerB() {
  const rows = new Array(1_000_000).fill('row');
  const count = rows.length;          // 必要な値だけ取り出す
  handlersB.push(() => count);        // rows は参照しない
}
for (let i = 0; i < 10; i++) registerB();
console.log('B 登録後:', mb());
B 登録後: 3.7 MB

クロージャを10個保持したままでも、メモリは増えていません。

知っておくと役立つ:使っていない内側関数が掴んでいる場合

const handlersC = [];
function registerC() {
  const rows = new Array(1_000_000).fill('row');
  const unused = () => rows.length;   // どこにも渡していない
  handlersC.push(() => 'done');       // rows を使っていない
}
for (let i = 0; i < 10; i++) registerC();
console.log('C 登録後:', mb());
handlersC.length = 0;
console.log('C 解除後:', mb());
C 登録後: 80.0 MB
C 解除後: 3.7 MB

保持している関数(() => 'done')は rows を使っていないのに、80 MB のままでした。Node.js 22(V8)では、同じスコープで作られた内側の関数たちは、外側の変数を1つの入れ物で共有する動きになっており、どれか1つでも rows を参照していると、ほかの関数が生きている間も rows が残ります。大きなデータを扱う関数の中でクロージャを作るときは、関数を分けるか、使い終わった変数に null を代入しておくと確実です。

直し方2:タイマーとリスナーは解除する

function startPolling() {
  const buffer = new Array(1_000_000).fill('log');
  return setInterval(() => buffer.length, 1000);
}
const ids = [];
for (let i = 0; i < 10; i++) ids.push(startPolling());
console.log('D 起動中:', mb());

ids.forEach(clearInterval);
console.log('D 停止後:', mb());
D 起動中: 80.0 MB
D 停止後: 3.7 MB

setInterval や addEventListener に渡した関数は、解除するまで実行環境が参照し続けます。画面を切り替えたときや部品を取り除くときに clearInterval・removeEventListener を呼ぶと、クロージャごと解放されます。

メモリが増え続けるときのチェックリスト

確認すること 対処
クロージャが大きな配列・オブジェクト全体を参照していないか 必要な値だけ変数に取り出してから関数を作る
setInterval を止めているか 不要になったら clearInterval
イベントリスナーを解除しているか removeEventListener、または AbortController の signal でまとめて解除
メモ化のキャッシュが増え続けていないか 上限を設ける、オブジェクトがキーなら WeakMap
グローバルな配列や Map に関数を溜めていないか 使い終わったら取り除く

クロージャ自体がメモリを多く使うわけではありません。「何を覚えさせているか」と「いつ手放すか」を意識することがポイントです。

クロージャを使うかどうかの判断表

やりたいこと 向いている方法 理由
関数1つに小さな状態を持たせたい クロージャ 最も短く書ける
設定を変えた関数をいくつも作りたい クロージャ(関数ファクトリ) 引数で設定を渡すだけ
複数のメソッドと状態をまとめ、インスタンスを多く作る class(# プライベートフィールド) メソッドが prototype で共有される
ファイル内だけで使う値を隠したい ES モジュール(export しない変数) モジュールのスコープ自体が外から見えない
React で値を描画間で持ち越したい useState / useRef 再描画との連携が用意されている

よくある質問

Q. クロージャとは、ひとことで言うと何ですか?

A. 「関数」と「その関数が定義された場所の変数」の組み合わせです。関数が外へ持ち出されても、定義された場所の変数を使い続けられます。JavaScript の関数はすべてこの性質を持っています。

Q. クロージャはどんなときに使いますか?

A. 関数に状態や設定を覚えさせたいときです。この記事で紹介した、カウンター、プライベート変数、関数ファクトリ、debounce/throttle、メモ化、once、イベントハンドラの7つが代表的な使いどころです。map や setTimeout に渡すコールバックが外側の変数を使っていれば、それもクロージャです。

Q. クロージャとクラスはどう使い分けますか?

A. 関数1つと少しの状態で足りるならクロージャ、複数のメソッドを持つ部品をたくさん作るなら class が向いています。クロージャはインスタンスごとに関数を作るのに対し、class のメソッドは prototype で共有されます。

Q. クロージャを使うとメモリリークしますか?

A. クロージャを使うだけでは起きません。クロージャが大きなデータを参照したまま、タイマーやイベントリスナー、グローバルな配列などに残り続けると、そのデータが解放されなくなります。必要な値だけを取り出す、不要になったら解除する、の2点で防げます。

まとめ

  • クロージャは「関数+定義された場所の変数」の組み合わせ。関数が生きている間、その変数も生き残る
  • 変数は内側から外側へ探され(スコープチェーン)、探す先は「書いた場所」で決まる
  • 使いどころは7つ:カウンター、プライベート変数、関数ファクトリ、debounce/throttle、メモ化、once、イベントハンドラ
  • 覚えているのは値のコピーではなく変数そのもの。for + var で [3, 3, 3] になるのはこのため。let で解決する
  • React の stale closure は「描画ごとに変数が別」なことが原因。setCount(c => c + 1)・依存配列・useRef で直せる
  • メモリは「何を覚えさせるか」と「いつ手放すか」で決まる。必要な値だけ取り出し、タイマーとリスナーは解除する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript クロージャとは?使いどころ7例をコード付きで図解」

株式会社RJCでは、JavaScriptのクロージャやスコープの仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?