こんにちは、かつコーチです。
前回はデフォルト引数の使い方を解説しました。
今回のテーマは「クロージャ」です。
JavaScriptを学んでいると必ずどこかで出会う言葉ですが、名前の響きに反して難しそうに見えてしまい、苦手意識を持つ人が多いテーマでもあります。
今日はできるだけ具体的なコードを見ながら、順番に理解していきましょう。
クロージャとは?
関数が「外側の変数」を覚えている仕組み
クロージャとは、ある関数が、自分の外側にあるスコープの変数を「覚えたまま」使い続けられる仕組みのことです。
言葉だけだと分かりにくいので、まずコードを見てみましょう。
function createCounter() {
let count = 0;
return function () {
count = count + 1;
return count;
};
}
const counter = createCounter();
console.log(counter());
// 出力: 1
console.log(counter());
// 出力: 2
console.log(counter());
// 出力: 3
createCounter の中で定義されている count という変数は、本来なら関数の実行が終わった時点で消えてしまうはずの変数です。
ところが、内側の関数(return されている無名関数)がその count を使い続けているため、createCounter の実行が終わった後も count は消えずに保持され続けます。
これが「関数が外側の変数を覚えている」というクロージャの正体です。
なぜこの仕組みが生まれるのか
JavaScriptの関数は、自分が定義された場所(スコープ)の変数に、実行後もアクセスし続けられる性質を持っています。
counter という変数には、createCounter の中で定義された内側の関数がそのまま入っています。
この内側の関数は「count という変数がある環境」ごと持ち運ばれているようなイメージで、これを英語で「閉じ込める(close over)」と表現することから「クロージャ(closure)」と呼ばれています。
基本の書き方
状態を保持するカウンター
先ほどのカウンターの例を、もう少し実用的な形にしてみましょう。
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment: function () {
count = count + 1;
return count;
},
decrement: function () {
count = count - 1;
return count;
},
getCount: function () {
return count;
},
};
}
const myCounter = createCounter(10);
console.log(myCounter.increment());
// 出力: 11
console.log(myCounter.increment());
// 出力: 12
console.log(myCounter.decrement());
// 出力: 11
console.log(myCounter.getCount());
// 出力: 11
count という変数に、外部から直接アクセスすることはできません。
increment や decrement といった、あらかじめ用意された関数を通してしか count を変更できないようになっています。
これは「変数を外から勝手に書き換えられないようにしたい」という場面でとても役立つ性質です。
複数のクロージャは、それぞれ別の記憶を持つ
createCounter を複数回呼び出すと、それぞれが独立した count を持ちます。
const counterA = createCounter();
const counterB = createCounter();
counterA.increment();
counterA.increment();
counterB.increment();
console.log(counterA.getCount());
// 出力: 2
console.log(counterB.getCount());
// 出力: 1
counterA と counterB は同じ関数から作られていますが、それぞれ別の count を覚えているため、お互いに影響を与えません。
関数を呼び出すたびに、新しい「記憶領域」が作られるとイメージすると分かりやすいです。
つまずきやすいポイント:for ループとクロージャ
var を使うと全部同じ値になってしまう
私が実際につまずいたのが、for ループの中でクロージャを作ったときの挙動です。
ボタンを何個か作って、それぞれクリックしたときに「自分の番号」を表示させたいだけのシンプルな処理のつもりが、なぜか全部同じ番号が表示されてしまうという不具合に何時間も悩まされました。
❌ Before:var を使ったことで全部同じ値を参照してしまう
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function () {
console.log(i);
});
}
funcs[0]();
// 出力: 3(0を期待していた)
funcs[1]();
// 出力: 3(1を期待していた)
funcs[2]();
// 出力: 3(2を期待していた)
var で宣言された i は、for ループ全体で1つしか存在しません。
そのため、3つの関数はすべて「同じ i という変数」を覚えてしまい、ループが終わった時点の最終的な値(3)を、3つとも参照してしまいます。
当時の私は「関数を作った時点の i の値がそれぞれ記憶されるはず」と思い込んでいたので、この挙動の原因が分からず、console.log をあちこちに仕込んでようやく気づいた記憶があります。
✅ After:let を使ってループごとに新しい変数を作る
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function () {
console.log(i);
});
}
funcs[0]();
// 出力: 0
funcs[1]();
// 出力: 1
funcs[2]();
// 出力: 2
let で宣言すると、for ループが1周するごとに、新しい i がその都度作られます。
そのため、3つの関数はそれぞれ「自分が作られたときの i」を個別に覚えることができ、期待通りの結果になります。
var と let のスコープの違いが、クロージャの挙動にそのまま直結する典型的な例です。
ループの中で関数を作るときは、var ではなく let を使うことを徹底しておくと、この種のバグを未然に防げます。
応用:関数を使ってプライベートな変数を作る
モジュールパターン
クロージャを使うと、外部から直接触られたくない変数を「隠す」ことができます。
これを応用した書き方は「モジュールパターン」と呼ばれ、設定値の管理などによく使われます。
function createConfig() {
let secretKey = "unpublished-key-123";
let isDebugMode = false;
return {
getIsDebugMode: function () {
return isDebugMode;
},
toggleDebugMode: function () {
isDebugMode = !isDebugMode;
},
// secretKeyを返す関数は用意しない = 外部から直接読めなくする
};
}
const config = createConfig();
console.log(config.getIsDebugMode());
// 出力: false
config.toggleDebugMode();
console.log(config.getIsDebugMode());
// 出力: true
console.log(config.secretKey);
// 出力: undefined(そもそも外部からアクセスする手段がない)
secretKey を返す関数を用意していないため、createConfig の外側からは secretKey の値を直接読み書きする方法がありません。
「必要な操作だけを外に公開して、内部の状態は隠しておく」という設計は、規模の大きいプログラムを整理するうえで欠かせない考え方です。
まとめ
この記事のポイント
- クロージャは、関数が自分の外側の変数を覚えたまま使い続けられる仕組み
- 関数を呼び出すたびに、それぞれ独立した記憶領域が作られる
forループの中でクロージャを作るときはvarではなくletを使うvarは全ループ共通の変数、letはループごとに新しい変数が作られる点が挙動の違いを生む- クロージャを使うと、外部から直接触られたくない変数を隠す設計ができる
次に読むべき記事
次回は「thisの挙動を理解する」を解説します。
クロージャと合わせて理解しておくと、JavaScriptの関数まわりの仕組みがぐっと整理されるテーマです。
→ 次の記事:thisの挙動を理解する