クロージャは「覚えている関数」
JavaScript で関数を定義するたびに、その関数はまわりにある変数への参照をこっそり持ち続けます。そして後からその関数を呼び出したとき、たとえまったく別の場所で実行されていても、元の変数にちゃんとアクセスできる。これが JavaScript のクロージャです。
いちばん短い例で見てみましょう。
makeGreeter が実行されて内側の関数を返し、そこで処理が終わります。普通に考えれば、ローカル変数の name は関数の実行が終わった時点で消えるはず。ところが、返された内側の関数が name を使い続けているため、JavaScript はその変数を生かし続けます。greetAda は "Ada" を覚えているし、greetBoris は "Boris" を覚えている。2 つのクロージャが、それぞれ別の値を抱え込んでいるわけです。
クロージャの正体はレキシカルスコープ
クロージャを支えているルールが、いわゆる レキシカルスコープ(lexical scope) です。関数が参照できる変数は、「呼び出された場所」ではなく「書かれた場所」で決まる、というルール。「レキシカル」という言葉は、要は「ソースコード上のどこに書かれているか」という意味にすぎません。
show が出力するのは "外側にいます" で、"caller の中にいます" ではありません。関数が 書かれた 場所がトップレベルの outer の隣だったので、参照されるのはそちらの outer です。たまたま自分用の outer を持っている場所から呼び出したとしても、関係ありません。
クロージャとは要するに、外側の関数の寿命を超えて生き残るレキシカルスコープのことです。誰かがまだ参照を握っているから、変数は消えずに残り続ける、というわけですね。
呼び出しごとに別々のクロージャができる
外側の関数を呼び出すたびに変数は新しく作られ、その呼び出しから返された内側の関数は その時の 変数を覚えています。さっきの greetAda と greetBoris がぶつからなかったのは、このためです。
クロージャの定番といえば、やっぱりカウンターでしょう:
a と b はそれぞれ自分専用の count を抱えています。返された関数の外からはこれらの変数に一切触れられず、count は完全にプライベートです。これは特別な言語機能を有効にしたわけではなく、クロージャの仕組みから自然とそうなるんです。
クラスを使わずにプライベート変数を実現する
クロージャに閉じ込められた変数は、返された関数を通してしか触れません。この性質を活かすと、本当の意味でプライベートな状態を持つ小さなオブジェクトを作ることができます。
balance は返されたオブジェクトのプロパティではなく、クロージャの内部に閉じ込められています。読み書きできるのは、公開したメソッド経由のみ。#private フィールド付きのクラスでも同じようなことはできますが、クロージャによるこのやり方は何十年も前から使われていて、今でもあちこちのコードで目にします。
ループでハマりがちな落とし穴
クロージャで一番つまずきやすいのが、ループの中で使うケースです。まずは var を使った場合を見てみましょう。
0、1、2 が出てくると思いきや、実際には 3、3、3 が返ってきます。理由はこうです。var は関数スコープなので、ループ全体で i は 1 つだけ しか存在しません。3 つのクロージャは同じ変数を参照していて、実行される頃にはループが終わって i が 3 になっているわけです。
そこで let に変えてみましょう。
今度はちゃんと 0、1、2 と出力されます。let はブロックスコープなので、ループが一周するたびに i の新しいバインディングが作られ、それぞれのクロージャが自分専用の値をキャプチャするんです。これこそが、var よりも let を使うべき最大の理由と言ってもいいでしょう。
クロージャがキャプチャするのは「値」ではなく「変数」
ちょっと分かりにくいけれど大事なポイントがあります。クロージャが保持しているのは 変数そのもの であって、関数が定義された時点の値のスナップショットではない、ということです。
printMessage は、作られた時点ではなく実行された瞬間に message を読みに行きます。その時の値をスナップショットとして残したいなら、まずローカル変数にコピーしておきましょう。実は for ループの中の let がやっているのは、これとほぼ同じことなんです。
実践でよく使うパターン:一度だけ実行する関数
クロージャを使った便利な小ネタとして、関数を1回しか実行させないようにするユーティリティを紹介します。
called と result は、返された関数が生きている間だけ存在するプライベートな状態です。グローバルなフラグも、余計なオブジェクトも要りません。小さなヘルパー、プライベートな状態、そしてクロージャ——このパターンはJavaScriptでもっとも役立つ機能の一つと言っていいでしょう。
メモリについて少しだけ
クロージャは、そのクロージャ自身がどこかから参照されている限り、キャプチャした変数を生かし続けます。たいていはそれが狙いなのですが、寿命の長いもの(DOMイベントリスナーやグローバルなキャッシュなど)にクロージャを結び付けていて、しかもそのクロージャが大きなデータをキャプチャしていると、クロージャが解放されるまでそのデータもガベージコレクションされません。注意しておきたいポイントです。
function attach() {
const hugeData = new Array(1_000_000).fill("...");
document.addEventListener("click", () => {
console.log(hugeData.length);
});
}
リスナーが登録されている間は、hugeData はメモリに居座り続けます。リスナーを外す(あるいは必要のないものは最初からキャプチャしない)ようにすれば、参照はクリアされます。細かく管理する必要はありませんが、クロージャとメモリは切っても切れない関係にある、ということだけは覚えておいてください。
この章のポイント
- クロージャとは、関数と、その関数が定義された時点で見えていた変数のセットのことです。
- 外側の関数を呼び出すたびに、内側のクロージャ用の変数セットが新しく作られます。
- クロージャを使えば、クラスを使わずにプライベートな状態を持たせられます。
- ループの中では
letを使い、各イテレーションごとに独立したバインディングを作りましょう。 - クロージャがキャプチャするのは変数そのものであって、生成時点の値ではありません。
次回予告: this キーワード
クロージャが扱うのは、関数の「周りにある変数」でした。次に見ていくのは、関数が何に対して呼び出されたか という話題です。JavaScript ではこれを this が担っていて、ここまで見てきたキャプチャ変数とはまったく違う振る舞いをします。
よくある質問
JavaScriptのクロージャとは何ですか?
クロージャとは、関数が定義されたときのスコープにある変数を、その外側のスコープが終わった後でも覚えている関数のことです。厳密に言えばJavaScriptの関数はすべてクロージャですが、実際に「クロージャ」という言葉が出てくるのは、関数が別の場所に渡されたり返されたりしても、元のスコープの変数を使い続けるようなケースです。
クロージャは何の役に立つのですか?
関数に「プライベートな状態」を持たせられるのが最大のメリットです。クラスやグローバル変数を使わずに、関数ごとにデータを閉じ込めておけます。具体的にはカウンター、一度しか実行されないコールバック、メモ化(memoize)ヘルパー、小さなAPIの裏に実装を隠すパターンなどでよく使われます。
ループの中でvarを使うとクロージャの挙動がおかしくなるのはなぜですか?
varを使うとクロージャの挙動がおかしくなるのはなぜですか?varは関数スコープなので、ループの全イテレーションで同じ変数を共有してしまいます。ループ内で作ったクロージャはすべてその1つの変数を参照するので、後からまとめて実行すると全部が最終値になってしまうわけです。解決策はletを使うこと。letはブロックスコープなので、イテレーションごとに別々のバインディングが作られます。