クロージャ
関数が定義時のスコープへの参照を抱えたまま持ち出され、外部から変数を参照できる仕組み
クロージャとは
クロージャ (Closure) は、関数が自分の定義位置のスコープチェーンへの参照を抱えたまま、値として外へ持ち出される仕組みである。抱えるのは変数の値のコピーではなく、変数が実際に置かれている領域 (仕様上の環境レコード) そのものへの参照で、同じスコープで作られた関数はその領域を共有する。
この「コピーでなく共有」が動作の分かれ目になる。外側の関数の実行が終わった後でも返された関数から変数を読み書きでき、書き換えは同じ領域を見る他の関数にもそのまま見える。JavaScript では関数が作られるたびにこの結び付きができるため、特別な構文なしに全ての関数がクロージャになる。
基本例
同じスコープを共有する 2 つの関数を返す形が最小の例になる。increment と getCount は count を各自にコピーして持つのではなく、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 ハンドラ | ハンドラ外の変数を再利用 |
| メモ化 | キャッシュをクロージャで保持 |
状態を共有する性質は裏返せば副作用にもなる。同じ入力で同じ結果を返す関数が欲しい場面では、外側の変数を書き換えないよう意識する必要がある (純粋関数・副作用)。
理論と実装の両面から学ぶなら関連書籍が参考になる。
この記事は役に立ちましたか?
関連用語
関連する記事
「動くコード」と「良いコード」の間にある本
コードが動くようになった後、次に何を学べばよいのか。「動くコード」を「良いコード」に変えるために必要な知識と、それを効率的に学べる本の選び方を解説します。
DevOps 本ガイド - CI/CD とインフラ自動化を学ぶ技術書の選び方
DevOps の文化と原則から CI/CD、IaC、オブザーバビリティまで学べる技術書の選び方と学習順序を紹介します。
深夜 3 時のデプロイ前に読み返したい 1 ページ
本番デプロイの直前、最終確認のチェックリストとして技術書の特定のページが役立つことがあります。緊張の場面で頼りになる「お守りの 1 ページ」の見つけ方と活用法。