クロージャでカウンタ
クロージャでカウンタ とは
関数の中の関数で状態を保持するクロージャを作る。本レッスンでは、クロージャでカウンタ の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。
クロージャで状態を持つ
JavaScript の関数は、 自分が作られた場所 の変数を覚えています。関数を返す関数を書いたとき、内側の関数が外側の変数を参照し続ける仕組み、これを クロージャ (closure) と呼びます。クロージャは JavaScript で 状態を持つ関数 を作るときの基礎部品です。
クロージャとは、関数とその関数が定義されたスコープの組み合わせのことです。内側の関数は外側の変数を
閉じ込めて持ち運べます。
慣れないうちは少し抽象的ですが、 カウンタ を作ってみると一気に身近になります。 呼ぶたびに 1, 2, 3, ... と数えていく関数を、グローバル変数を使わずに作る — そのための定番テクニックがクロージャです。
カウンタの実装例
以下が定番の書き方です。
JavaScript
function createCounter () {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3createCounter の内部で let count = 0 を宣言し、そこへアクセスする関数を return しています。 createCounter の呼び出しは 1 度きりですが、戻り値の関数を何度呼んでも count は生き続け、毎回 1 ずつ増えます。
let countは外からは触れません。これがクロージャによるカプセル化です。グローバル変数を使わずに状態を隠せます。
スコープのイメージ
内側の関数は let count というメモを持ったまま外に持ち出されます。 createCounter 自体は return 後に役目を終えるはずですが、内側の関数がまだ count を参照しているため、 count はガベージコレクションされずに残ります。
別々のカウンタを作る
createCounter() を 2 回呼べば、 独立した カウンタが 2 つできます。
JavaScript
const a = createCounter();
const b = createCounter();
console.log(a()); // 1
console.log(a()); // 2
console.log(b()); // 1 — b は別の count を持っている
console.log(a()); // 3このように 状態を 1 つずつ閉じ込めた関数 を量産できるのがクロージャの強みです。 クラス がなくてもオブジェクト指向ライクな書き方ができる、と捉えても良いでしょう。
JavaScript
// アロー関数版
const createCounter2 = () => {
let count = 0;
return () => ++count;
};よくある間違い
以下のとおりです。
countをcreateCounterの外にlet count = 0で書いてしまい、全インスタンスで共有されるreturn countを書き忘れて、毎回undefinedになるforループの中でvar iを使ったクロージャを作り、全て同じiを参照する古典的バグ (今はletで解決)
JavaScript
// var 版の古典バグ
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3 と出力される (i を共有しているため)
letでループ変数を宣言すれば、ループの各反復で別の束縛になるので0, 1, 2と期待通りに動きます。クロージャと letの関係はよく試験に出ます。
クロージャの応用例
クロージャは カウンタ 以外にもさまざまな場面で活躍します。 設定をまとめて閉じ込めたい 、 関数ファクトリーを作りたい 、 イベントハンドラに状態を渡したい 、こういった時に強力です。例えば n 倍する関数 を作るファクトリーは次のように書けます。
JavaScript
function makeMultiplier (n) {
return (x) => x * n;
}
const triple = makeMultiplier(3);
const tenTimes = makeMultiplier(10);
console.log(triple(4)); // 12
console.log(tenTimes(7)); // 70外側の関数の引数 n が内側の関数に閉じ込められているのがポイントです。 triple と tenTimes はそれぞれ別の n を覚えているので、独立した関数として動作します。
クロージャを意識して書くと、
グローバル変数への依存が減り、テストしやすくバグの少ないコードになります。
やってみよう
まず createCounter 関数の内部で let count = 0 を持ち、呼ぶたびに count を 1 増やして返す関数を返してください。今回のテストでは、その仕組みを使った関数 callNTimes(n) を書きます。
callNTimes(n) は createCounter() を呼んで得た関数を n 回呼び出し、最後の戻り値を return します。 callNTimes(0) のときは 0 を返してください。クロージャを使えば外部に状態をはみ出させずに実装できます。
よくある質問
Q. クロージャはどんな場面で使いますか?
A. プライベートな状態を外から触らせずに保持したいときに便利です。例えばカウンタ関数を return することで内部の count 変数を隠蔽できます。React の useState もクロージャを使って状態を保っており、関数型プログラミングの基礎概念のひとつです。
Q. クロージャでメモリリークは起きませんか?
A. 外側の変数を保持し続けるため、巨大な配列などを掴んだままだとリークの原因になります。不要になったら関数参照を null にして GC 対象にする、イベントリスナーを removeEventListener で外す、などの対策が有効です。
Q. ループ内でクロージャを作るときの罠は?
A. var を使うと全クロージャが同じ変数を共有してしまい、最後の値しか参照しなくなります。let を使えば毎ループごとに新しい束縛になるので意図通りに動きます。古いコードを読む際に頻発するハマりどころなので覚えておきましょう。
次のレッスン
次は Array.map で、関数の中の関数で状態を保持するクロージャを作る を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- クロージャ の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. クロージャ とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- 関数の中で createCounter のような関数を定義する (クロージャを使う)
- 内側の関数は let count などの状態を保持し、呼ぶたびに 1 ずつ増やした値を返す
- callNTimes(n) は counter を n 回呼んで、最後の戻り値を return する
入出力例
test-cases.txt
callNTimes(0) → 0
callNTimes(1) → 1
callNTimes(3) → 3
callNTimes(10) → 10