基礎●●●○○

クロージャ

Closure

関数が定義された場所の変数環境を捕まえたまま持ち歩く仕組み。状態の隠蔽やコールバックの土台になる。

概要

クロージャは、関数が「自分が定義された場所で見えていた変数」を捕まえたまま持ち歩く仕組みです。関数を値として別の場所に渡したり後で実行したりしても、定義時に参照していた外側の変数には引き続きアクセスでき、しかもその変数は読むだけでなく更新もできます。関数と、その関数が生まれた環境(変数の束)のセット — それがクロージャです。

JavaScript を書いていれば、意識せずとも毎日クロージャを使っています。イベントハンドラが外側で宣言した変数を参照するとき、コールバックがリクエスト時の情報を覚えているとき、その裏で働いているのはすべてこの仕組みです。Python・Ruby・Swift・Kotlin・Rust など、関数を値として扱えるモダンな言語のほぼすべてが備えています。

「関数に状態を持たせる」最小の道具でもあり、関数型プログラミングの部品の合成から、状態管理ライブラリや React のフックまで、現代のプログラミングの広い範囲を静かに支えています。

なぜ生まれたか

関数を値として渡せる「第一級関数」を言語に入れると、避けて通れない問題が生じます。関数が外側のスコープの変数を参照しているまま、そのスコープの外へ持ち出されたらどうなるか、という問題です。通常、ローカル変数は関数の実行が終わればコールスタックから消えます。しかし内側の関数がその変数を参照したまま外へ返されると、「すでに終了した関数の変数」への参照が残ってしまう。初期の LISP はここで、呼び出し時点のスコープで変数を解決する「動的スコープ」を採り、定義時の意図とは違う変数を掴んでしまう混乱(funarg 問題と呼ばれます)を抱えていました。

1960〜70年代の研究(言語 Scheme が決定打です)が出した答えが、「関数を作るときに、定義場所の環境ごと閉じ込める(close over する)」という設計でした。クロージャという名前はこの「閉じる」に由来します。変数の寿命を「スコープを抜けたら終わり」ではなく「参照される限り生きる」に変えることで、関数を安心して値として持ち運べるようになったのです。

詳細

レキシカルスコープ — 「どこで定義されたか」がすべて

クロージャの土台はレキシカルスコープ(静的スコープ)です。関数の中で外側の変数名が出てきたとき、それがどの変数を指すかは「どこから呼ばれたか」ではなく「ソースコード上のどこで定義されたか」で決まる、という規則です。コードを読むだけで参照先が確定するため人間にも処理系にも扱いやすく、現代の主要言語はほぼすべてこちらを採用しています。クロージャとは、このレキシカルスコープを「関数がスコープの外に持ち出された後」まで一貫して守り抜く仕組みだと言えます。

定番のカウンタの例が、仕組みの核心を最小のコードで見せてくれます。

function makeCounter() {
  let count = 0;
  return () => ++count;
}
const counter = makeCounter();
counter(); // 1
counter(); // 2

makeCounter の実行はとっくに終わっているのに、返された関数を呼ぶたびに count は増え続けます。変数 count が、関数と一緒に生き延びているのです。

makeCounter() を実際に何度も呼んで、独立した環境と共有された環境の違いを手元で確かめられます。

⚡ 体験: makeCounter() でクロージャを量産する
function makeCounter() {
  let count = 0;          // ← 各呼び出しで新しく作られる
  return () => ++count;   // ← count を閉じ込めた関数を返す
}

まだクロージャがありません

共有された環境 — 2つの関数が同じ count を閉じ込めた場合
let count = 0;  // ← 1つの環境に1回だけ定義
const increment = () => ++count;
const reset     = () => { count = 0; };
共有の環境レコードcount: 0

「makeCounter() を呼ぶ」を押すと、関数オブジェクトと環境レコードのペア — クロージャ — が1つ生まれます。

変数はどこで生きているのか

これを可能にしているのがメモリ管理の工夫です。通常のローカル変数はスタック上のフレームに置かれ、関数終了とともに消えます。しかし処理系は、内側の関数に捕まえられた変数を検出すると、それをヒープ上の「環境レコード」に置きます。関数オブジェクト自体もヒープ上の値であり、コードへの参照に加えてこの環境レコードへの参照を持ちます。スタックフレームが消えても、関数オブジェクトから環境への参照が残っている限り、ガベージコレクションは変数を回収しません。「変数の寿命がスコープではなく参照で決まる」という性質は、GC があってこそ安全に成立するのです。

スタック呼び出し元のフレームconst counter = …makeCounter のフレーム実行終了 → すでに消滅ヒープ関数オブジェクト counterコード + 環境への参照環境レコードcount: 2変数が参照環境を捕獲×この参照は消えたが関数からの参照が残る限り count は回収されない
クロージャの構造 — 外側の関数のフレームが消えても、捕獲された変数はヒープ上で生き続ける

コールバックとの相性 — クロージャが日常になる場所

クロージャが最も活躍するのは、コールバックやイベントハンドラのように「関数を後で・別の場所で実行する」場面です。ボタンのクリックハンドラがそのボタンに対応するデータを覚えている、非同期処理の完了時にリクエスト開始時の文脈で続きを実行できる — 「そのときの文脈を関数に同梱して未来へ送る」ことがクロージャの実用上の本質です。引数として明示的に文脈を引き回す必要がないため、JavaScript の非同期プログラミングはこの仕組みなしには成立しません。

古典的な罠も、この「同梱」の理解を試します。ループの中でハンドラを登録し、ループ変数を参照させると、全ハンドラが同じ1つの変数を共有してしまい、実行時にはすべてループ終了後の最終値を見る — という定番のバグです。クロージャが捕まえるのは「その時点の値のコピー」ではなく「変数そのもの」だからです。JavaScript では varlet に変えるだけで反復ごとに新しい変数が作られて解決しますが、Python のループや Go の古いバージョンでも同型の罠があり、「値を捕まえたいのか、変数を捕まえたいのか」を意識する良い教材になっています。

状態の隠蔽 — もうひとつのカプセル化

カウンタの例をもう一度見ると、count には返された関数を通す以外のアクセス手段が一切ありません。外から直接読むことも書き換えることもできない — つまりクロージャは、オブジェクト指向がクラスの private フィールドで実現するカプセル化を、関数だけで実現しています。「クロージャは貧者のオブジェクト、オブジェクトは貧者のクロージャ」という古い警句が示すとおり、両者は「状態と振る舞いを束ねる」という同じ問題への別解です。実際、JavaScript のモジュールパターンや React のフックは、クラスではなくクロージャで状態を隠蔽する設計の実例です。ただし力には代償もあり、うかつに大きなオブジェクトを捕まえた長寿命のクロージャは、GC がそれを回収できずメモリリークの原因になります。「この関数は何を捕まえているか」を意識することが、クロージャを使いこなす最後の一歩です。