JavaScript を学ぶと、クロージャの説明で一度はつまずく人が多いです。「関数の中に関数がある」「外の変数が見えている」——文法は追えても、なぜそうなるのかが腹落ちしにくい、という声もよく聞きます。
この記事では、クロージャを「難しい概念」として暗記するのではなく、スコープの延長線上にある仕組みとして理解できるよう、基本からよくあるパターンまで順にまとめます。
この記事でわかること
- クロージャとは何か(一言定義とイメージ)
- レキシカルスコープとの関係
- カウンター・ループ・コールバックなどよく出るパターン
- 「変数が全部同じ値になる」系のつまずきポイント
こんなときに読むと役立ちます
- クロージャという言葉は知っているが、説明できない
forループの中で関数を作ると、意図と違う値になる- イベントリスナーやコールバックで「外の変数」が残る理由がわからない
- 「プライベート変数」のような書き方を見て、何をしているか追いにくい
まず押さえる:クロージャとは
クロージャ(closure)とは、ざっくり言うと次のような仕組みです。
関数が、定義されたときの周囲の変数(スコープ)を「覚えたまま」動き続けること
ポイントは2つです。
- 関数は定義された場所のスコープを参照する(レキシカルスコープ)
- 外側の関数の実行が終わっても、内側の関数がその変数を保持し続けられる
最小の例
function createGreeter(name) {
return function () {
console.log('こんにちは、' + name);
};
}
const greetTaro = createGreeter('太郎');
greetTaro(); // こんにちは、太郎
createGreeter の実行はすでに終わっています。それでも greetTaro は引数 name(ここでは '太郎')を覚えています。これがクロージャです。
レキシカルスコープ:どこで書いたかが大事
JavaScript では、変数をどこで参照したかではなく、関数をどこで定義したかでスコープが決まります。これをレキシカルスコープと呼びます。
const message = '外側';
function outer() {
const message = '中側';
function inner() {
console.log(message); // 中側
}
return inner;
}
const fn = outer();
fn(); // 中側
inner は outer の中で定義されているので、outer 内の message を見ます。グローバルの message ではありません。
よくあるパターン
1. カウンター(状態を閉じ込める)
外から直接いじれない「内部の数値」を持たせたいときに使われます。
function createCounter() {
let count = 0;
return {
increment() { count++; },
getCount() { return count; },
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
count は外から直接アクセスできませんが、返したメソッドがクロージャとして count を保持しています。
2. 設定を覚えた関数を返す(ファクトリ)
function multiplyBy(factor) {
return function (value) {
return value * factor;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
double と triple は、それぞれ別の factor を覚えています。
3. イベントやコールバック
クリック時に「そのときの値」を使いたい場面でもクロージャが効きます。
function setupButton(label) {
const button = document.createElement('button');
button.textContent = label;
button.addEventListener('click', function () {
console.log(label + ' がクリックされた');
});
return button;
}
コールバック関数が、定義時の label を覚えたまま、後からクリックに応答します。
つまずきポイント:ループと var
クロージャの説明でいちばん多いのが、「ループで作った関数が全部同じ値を出す」パターンです。
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// 期待: 0, 1, 2
// 実際: 3, 3, 3
理由は、var が関数スコープでループ全体を共有し、タイマー実行時には i がすでに 3 だからです。コールバックは各回の i を個別に覚えているのではなく、同じ i を参照し続けている状態になります。
対処法1:let を使う(ブロックスコープ)
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// 0, 1, 2
let はループの各回ごとに別の i を作るため、コールバックが覚える値も分かれます。
対処法2:IIFE で値を固定する(昔の書き方)
for (var i = 0; i < 3; i++) {
(function (fixed) {
setTimeout(function () {
console.log(fixed);
}, 100);
})(i);
}
即時関数に i を渡し、その回の値を fixed として閉じ込めます。
覚えておきたいこと
クロージャは「変数を覚える」仕組みなので、ループで共有されている変数をそのまま参照すると、意図とずれやすいです。let や引数でのコピーで「そのときの値」を分離する、と考えると整理しやすいです。
よくある誤解
| 誤解 | 実際 |
|---|---|
| クロージャ=関数の中に関数がある | 入れ子はよくあるが、本質は外側の変数を保持できること |
| クロージャは特別な構文 | 普通の関数でも、条件を満たせばクロージャになる |
| 外側の変数はいつまでも残る | 参照がなくなればガベージコレクションの対象になる |
読み方のコツ
コードを見たときは、次の順で追うと理解しやすいです。
- 内側の関数はどこで定義されているか
- その外側で、どの変数・引数が使われているか
- 返された関数やコールバックが、いつ実行されるか(その時点で何を覚えているか)
「この関数は、生まれたときのどの変数を覚えているか?」と自分に問いかけると、クロージャの読み解きが一気に楽になります。
まとめ
- クロージャ=関数が定義時のスコープを保持し続ける仕組み
- スコープは定義場所で決まる(レキシカルスコープ)
- カウンター・コールバック・設定済み関数でよく使われる
- ループでは
varの共有に注意。letや引数で値を分離する
次に function が入れ子になったコードを見たら、まず「内側は、外側のどの変数を覚えているか?」とたどってみてください。クロージャは暗記より、変数の寿命と参照の向きを追う練習で身につきます。
