改めて整理するクロージャ

javascriptclosurefundamental

整理することになったきっかけ

最近、NAVER Webtoonの体験型インターン面接を受けた。

初めての面接で、緊張していたせいか...CS関連の質問でしどろもどろしながら答えてしまった。

その中の一つがクロージャに関する質問だった。

そして、家に帰って考えてみると、クロージャについて誤った概念を答えていたことに気づいた。

二度と同じ失敗をしないように、クロージャについて改めて整理しようと思う。

クロージャ(Closure)の語源

英語でクロージャ(Closure)という言葉は本当に様々な意味で使われる。

辞書で検索すると、以下のような意味がある。

🤔Closure - 辞書より
  1. 名詞 (工場、学校、病院などの永久的な)閉鎖(される状況)
  2. 名詞 (道路・橋の一時的)閉鎖
  3. 名詞 (困難なことの)終了[終結]

Closeの名詞形として閉鎖、終了、終わり、閉じるなどの意味で使われる。

ただし、このように辞書的な意味だけで定義すると混乱が生じやすい。

JSでクロージャの語源は上記に基づいたものではなく、数学やコンピュータサイエンスで閉包を意味するClosureに由来しているからだ。

閉包は通常〜に閉じているという表現でよく使われる。

例えば、整数集合は加算演算に対して閉じているまたは整数集合は加算演算に対して閉包を持つのような表現で使われる。

閉包の図 1
閉包の図 1
閉包の図 2
閉包の図 2

整数(Integer)演算の場合、四則演算(+, -, *, /)をしてもその結果値は整数集合に再び該当する。

ある行為の結果が自身の集合に帰結することをClosureという。

JSではクロージャはどのように表現されるか?

クロージャ表現図
クロージャ表現図

JSではクロージャを上のように表現できる。

outer関数内部にあるinner関数がouter関数外部からouter関数のスコープ(scope)内にある変数aを参照することである。

クロージャに関する様々な表現

クロージャ(Closure)はJavaScript固有の概念ではない。

関数型プログラミング言語に登場する普遍的な特性である。

Rustでのクロージャ
Rustでのクロージャ

Rust公式ガイドからキャプチャした内容だ。

Rustでも見ての通り、クロージャを扱っているのがわかる。

JavaScript固有の概念ではないため、ECMAScript仕様でもクロージャの定義を扱っていない。

特にそのためではないが、様々な文献でクロージャをそれぞれ異なる方法で定義している。

🤔クロージャに関する様々な表現
  • 自身を内包する関数のコンテキストにアクセスできる関数
    - ダグラス・クロックフォード、<<JavaScript: The Good Parts>>、オライリー(p68)
  • 関数が特定のスコープにアクセスできるように意図的にそのスコープで定義すること
    - イーサン・ブラウン、<<Learning JavaScript>>、オライリー(p196)
  • 関数を宣言する時に作られる有効範囲が消えた後でも呼び出せる関数
    - ジョン・レシグ、<<Secrets of the JavaScript Ninja>>、マニング(p116)
  • すでにライフサイクルが終了した外部関数の変数を参照する関数
    - ソン・ヒョンジュ、コ・ヒョンジュン、<<Inside JavaScript>>、ハンビットメディア(p157)
  • 自由変数がある関数と自由変数を知ることができる環境の結合
    - エリック・フリーマン、<<Head First JavaScript Programming>>、オライリー(p534)
  • ローカル変数を参照している関数内の関数
    - 山田佳宏、<<JavaScriptマスターブック>>、技術評論社(p180)
  • 自身が生成される時のスコープで知ることができた変数の中で、いつか自身が実行される時に使用する変数だけを記憶して維持する関数
    - ユ・インドン、<<関数型JavaScriptプログラミング>>、インサイト(p31)

MDNの定義を通じて見てみる

🤔MDN(Mozilla Developer Network)での定義

"A closure is the combination of a function and the lexical environment within which that function was declared."
「クロージャは関数とその関数が宣言された当時のlexical environmentとの相互関係による現象」

MDNではクロージャを上記のように定義している。

ここでlexical environmentとは実行コンテキストの構成要素の一つであるouterEnvironmentReferenceを指す。

LexicalEnvironmentenvironmentRecordouterEnvironmentReferenceによって変数の有効範囲であるスコープが決定され、スコープチェーンが可能になる。

あるコンテキストAで宣言した内部関数Bの実行コンテキストが活性化された時点を考えよう。

JavaScript

上記のようなコードがある時、関数A変数bにアクセスできないが、関数B変数aにアクセスできる。

これは関数BouterEnvironmentReference関数Aを参照しているからである。

これがまさに相互関係(combination)の意味である。

ただし、内部の関数B関数ALexicalEnvironmentを常に参照するわけではない。

内部関数で外部変数を参照しない場合はcombinationとは言えない。

内部関数で外部変数を参照する場合に限ってcombination、つまり宣言された当時のlexicalEnvironmentとの相互関係が意味を持つ。

クロージャ表現図
クロージャ表現図

再度上の図を見てみよう。

ここまで把握した内容によると、クロージャ(Closure)とは「ある関数で宣言した変数を参照する内部関数でのみ発生する現象」と見ることができる。

コードを通じて見てみる

コードを通じてもう少し見てみよう。

一般的な状況でのコールスタックの流れ

JavaScript

4行目にあるconsole.log(++a);の結果である2が最終的に出力される。

outer関数の実行コンテキストが終了するとLexicalEnvironmentに保存された識別子(a, inner)への参照を削除する。

それに従い、各アドレスに保存されていた値は自身を参照する変数が一つもなくなるため、ガベージコレクタの収集対象となる。

初期状態
初期状態

VariableEnvironmentおよびThisBindingは省略したコールスタックと実行コンテキストを図式化した図である。

一般的な関数内部での動作であり、特に特別な現象は見られない。

クロージャが発生する状況でのコールスタックの流れ

JavaScript

6行目でinner関数を実行した結果をreturnしている。

結果的にouter関数の実行コンテキストが終了した時点では変数aを参照する対象がなくなる。

前の例と同様にa, inner変数の値はいつかガベージコレクタによって消滅する。

ここまではouter関数の実行コンテキストが終了する前にinner関数の実行コンテキストが終了する。

それに従い、別途inner関数を呼び出すことはできない。

inner関数の実行コンテキストのenvironmentRecordには収集する情報がない。

outerEnvironmentReferenceにはinner関数が宣言された位置のLexicalEnvironmentが参照コピーされる。

inner関数outer関数内部で宣言されたため、outer関数LexicalEnvironmentが含まれる。

スコープチェーン(Scope Chaining)に従ってouter関数で宣言した変数aにアクセスして1増加させた後、その値である2を返し、inner関数のコンテキストが終了する。

10行目も同様の方法で3を返す。

ガベージコレクタの原理

🤔ガベージコレクタ(Garbage Collector)の原理

ガベージコレクタはある値を参照する変数が一つでもあれば、その値は収集対象に含めない。

inner関数の実行コンテキストが活性化されるとouterEnvironmentReferenceouter関数LexicalEnvironmentを必要とするため、収集対象から除外される。

そのおかげでinner関数変数aにアクセスできる。

コールスタックの流れを図で理解する

初期状態
初期状態

まとめ

先にクロージャはある関数で宣言した変数を参照する内部関数でのみ発生する現象だと述べた。

現象だということを忘れないでほしい。クロージャは関数ではない。

最後に見たことを思い出すと、ここではouter関数LexicalEnvironmentに属するもののうち変数aが対象から除外された。

このように関数の実行コンテキストが終了した後、LexicalEnvironmentがガベージコレクタの収集対象から除外されるケースはローカル変数を参照する内部関数が外部に伝達された場合が唯一である。

「ある関数で宣言した変数を参照する内部関数でのみ発生する現象」とは 「外部関数のLexicalEnvironmentがガベージコレクションされない現象」を意味する。

これを基に定義を修正すると以下のようになる。

🤔クロージャ(Closure)の定義

クロージャとはある関数Aで宣言した変数aを参照する内部関数Bを外部に伝達した場合、Aの実行コンテキストが終了した後も変数aが消えない現象

クロージャ表現図
クロージャ表現図

上の概念を持った状態で図を再度見てみよう。

今は理解できるだろうか?

注意事項

外部への伝達は果たしてreturnだけを意味するのか?

コードで見てみよう。

JavaScript

別の外部オブジェクトであるDOMメソッド(addEventListener)に登録するhandler関数内部でローカル変数を参照している。

両方の状況ともローカル変数を参照する内部関数を外部に伝達したためクロージャである。

参考資料

コアJavaScript