クロージャ

関数が定義時のスコープへの参照を抱えたまま持ち出され、外部から変数を参照できる仕組み

JavaScript関数型プログラミング

クロージャとは

クロージャ (Closure) は、関数が自分の定義位置のスコープチェーンへの参照を抱えたまま、値として外へ持ち出される仕組みである。抱えるのは変数の値のコピーではなく、変数が実際に置かれている領域 (仕様上の環境レコード) そのものへの参照で、同じスコープで作られた関数はその領域を共有する。

この「コピーでなく共有」が動作の分かれ目になる。外側の関数の実行が終わった後でも返された関数から変数を読み書きでき、書き換えは同じ領域を見る他の関数にもそのまま見える。JavaScript では関数が作られるたびにこの結び付きができるため、特別な構文なしに全ての関数がクロージャになる。

基本例

同じスコープを共有する 2 つの関数を返す形が最小の例になる。incrementgetCountcount を各自にコピーして持つのではなく、createCounter の 1 回の呼び出しで作られた同一の領域を見る。だから一方の更新がもう一方の読み取りに反映される。

function createCounter() {
  let count = 0; // この変数がクロージャで記憶される
  return {
    increment: () => ++count,
    getCount: () => count,
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount();  // 2
// count は外部からアクセスできないが、返された関数からは参照可能

なぜ動くのか

関数呼び出しごとに変数の置き場が新しく作られ、その置き場への参照が返された関数側に残る、という順序で理解すると迷わない。

createCounter() 呼び出しごとに:
  1. 新しい変数の置き場が作られ count = 0 が置かれる
  2. increment, getCount はその置き場への参照を持つ
     (count の値を写し取るのではない)
  3. createCounter() の呼び出しは終了するが、
     返された関数から置き場へ到達できるため回収されない
  4. 2 つの関数は同じ置き場を見るので count の更新が共有される

置き場は呼び出しごとに別なので、createCounter() を 2 回呼べばカウンタは互いに独立する。逆に、返された関数を保持し続ける限り置き場もガベージコレクションの対象にならない。大きな配列を抱えたスコープからコールバックを 1 つ外に出しただけでその配列が残り続ける、という形でメモリが伸びるのはこの性質が原因である。

実用例: プライベート変数

引数もスコープの一部なので、そのままプライベートな設定値として使える。下の prefix は返された関数からしか読めず、呼び出し側に書き換える手段がない。クラスの private フィールドを使わずに隠蔽したいときの定番の形である。

function createLogger(prefix: string) {
  return (message: string) => console.log(`[${prefix}] ${message}`);
}
const log = createLogger('API');
log('Request received'); // [API] Request received
// prefix は外部から変更できない

実用例: イベントハンドラ

コールバックは登録した後、元の関数の実行が終わってから呼ばれる。その時点でも登録時のスコープが生きているため、状態をグローバル変数や DOM 属性に退避させずにハンドラ内へ閉じ込められる。

function setupButton(id: string) {
  let clickCount = 0;
  document.getElementById(id)?.addEventListener('click', () => {
    clickCount++;
    console.log(`Clicked ${clickCount} times`);
  });
  // コールバック関数が clickCount をクロージャで記憶
}

よくある落とし穴: ループとクロージャ

「値のコピーではなく置き場の共有」という機序が最も表に出るのがループである。var の宣言は関数単位なので反復ごとに新しい変数は作られず、3 つのコールバックはどれも同じ置き場の同じ i を指す。ループを抜けた時点で i は 3 になっており、100 ミリ秒後に走るコールバックはその共有された値を読むため 3 が 3 回出る。let は反復ごとに別の束縛を作るので、各コールバックが見る置き場も別になり 0, 1, 2 が出る。

// ❌ var はブロックスコープを持たない
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// ✅ let はブロックスコープ (各反復で新しい変数)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2
}

Rust のクロージャ

JavaScript が常に置き場を共有するのに対し、Rust はキャプチャの仕方を型で区別する。読むだけなら不変参照 (&T)、書き換えるなら可変参照 (&mut T)、所有権が必要なら move で値ごと取り込む。コンパイラは本体の使い方から Fn / FnMut / FnOnce のどれを実装するかを決め、これを引数の境界として書けるため、共有か移動かがシグネチャに現れる。所有権を外へ移すクロージャは 1 回しか呼べない FnOnce だけになる。

let x = 5;
let add_x = |n| n + x;  // x をキャプチャ
add_x(3) // 8

// Rust はキャプチャ方法を区別: &T, &mut T, T (所有権の移動)
let add_x = move |n| n + x; // x の所有権を移動

クロージャの活用場面

用途はどれも「状態を持つ関数を安全に配る」形に収まる。判断基準は、状態の寿命を関数の寿命に合わせたいかどうかである。

場面説明
プライベート変数外部からアクセスできない変数
イベントハンドラコールバックで外部変数を参照
カリー化引数を部分適用
Lambda ハンドラハンドラ外の変数を再利用
メモ化キャッシュをクロージャで保持

状態を共有する性質は裏返せば副作用にもなる。同じ入力で同じ結果を返す関数が欲しい場面では、外側の変数を書き換えないよう意識する必要がある (純粋関数副作用)。

理論と実装の両面から学ぶなら関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事