A Technical Note

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

javascriptclosurefundamental
DEV.CLO
改めて整理するクロージャ
Fig. I — 関連スクリーンショット。
Abstractum · 要旨

関数型プログラミングの特徴の一つであるクロージャ。誤解していた部分を改めて整理します。

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

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

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

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

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

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

クロージャ(Closure)の語源

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

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

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

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

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

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

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

閉包の図 1
Fig. — 閉包の図 1
閉包の図 2
Fig. — 閉包の図 2

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

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

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

クロージャ表現図
Fig. — クロージャ表現図

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

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

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

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

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

Rustでのクロージャ
Fig. — Rustでのクロージャ

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

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

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

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

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

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

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

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

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

JavaScript

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

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

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

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

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

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

クロージャ表現図
Fig. — クロージャ表現図

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

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

コードを通じて見てみる

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

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

JavaScript

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

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

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

初期状態
Fig. — 初期状態

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を返す。

ガベージコレクタの原理

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

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

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

初期状態
Fig. — 初期状態

まとめ

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

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

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

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

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

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

クロージャ表現図
Fig. — クロージャ表現図

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

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

注意事項

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

コードで見てみよう。

JavaScript

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

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

参考資料

コアJavaScript

MMXXVI · 2024-01-07
読者の余白· Commentarii