クロージャでカウンタ

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

クロージャでカウンタ とは

関数の中の関数で状態を保持するクロージャを作る。本レッスンでは、クロージャでカウンタ の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

クロージャで状態を持つ

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()); // 3

createCounter の内部で let count = 0 を宣言し、そこへアクセスする関数を return しています。 createCounter の呼び出しは 1 度きりですが、戻り値の関数を何度呼んでも count は生き続け、毎回 1 ずつ増えます。

let count は外からは触れません。これがクロージャによる カプセル化 です。グローバル変数を使わずに状態を隠せます。

スコープのイメージ

diagram (will load when visible)

内側の関数は 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; };

よくある間違い

以下のとおりです。

  • countcreateCounter の外に 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 が内側の関数に閉じ込められているのがポイントです。 tripletenTimes はそれぞれ別の 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 つができることを確認しましょう。

  1. クロージャ の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. クロージャ とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. 関数の中で createCounter のような関数を定義する (クロージャを使う)
  2. 内側の関数は let count などの状態を保持し、呼ぶたびに 1 ずつ増やした値を返す
  3. callNTimes(n) は counter を n 回呼んで、最後の戻り値を return する

入出力例

test-cases.txt

callNTimes(0)0 callNTimes(1)1 callNTimes(3)3 callNTimes(10)10

ヒント

main.js
main.js
学習モード

メモ

クロージャでカウンタ

⌘S で保存