詳細 ECMA-262-3 第6章 クロージャ
おつかれさまでございます。東洋大学柏原選手の好きな声優は花澤香菜さんですが、株式会社ミクシィ大形選手の好きな声優は五十嵐裕美さんです。お世話になります。
さて、 Dmitry 先生の ECMA-262-3 シリーズもついに山場、クロージャの章へとやって参りました。「 JavaScript はクロージャが使えて強力」「 JavaScript 理解のキモはクロージャ」などといった売り文句や脅し文句を耳にされたことは無いでしょうか。クロージャがなぜ強力なのか、そしてそれはどのような仕組みに基づくものなのか、これは ECMAScript だけに留まらず、一般的な意味でクロージャを理解できる名章です。どうぞお時間のある時に、気持ちを落ち着けて、ごゆっくりご覧ください。
この章では、 JavaScript に関して最も議論されているトピックの一つ、クロージャについてお話しします。実際のところこのトピックは目新しいものではありませんし、幾度となく議論され、そのエッセンスを扱った記事が既に数多く存在します(中にはとてもすばらしいものがあります。例えば、参考文献中に示す Richard Cornford 氏の記事です)。しかし、ここでまた私たちも、理論的な観点から議論し理解を深めてみることにしましょう。そして ECMAScript の内側から、クロージャがどのように作られているのかを見ていくことにしましょう。
これまでの章でも述べたように(全体に共通する話ですが)、各々の章はそれ以前の章に依存しています。必要があれば、この章を完全に理解するために、『第4章 スコープチェーン』、とおそらくはそれに関連するさらに前の章、『第2章 変数オブジェクト』を参照してください。
ECMAScript におけるクロージャを検討する前に、 ECMA-262-3 の仕様とは関係無く、関数型プログラミングにおける一般的な理論の観点から、クロージャにまつわる定義を行うことにします。ただしそれらの定義を説明するサンプルには、もちろん ECMAScript を用います。
ご存じの通り、関数型言語(そして ECMAScript はこのパラダイム及びスタイルをサポートしています)においては、関数とはデータです。すなわち、変数中に保存されることができ、他の関数に引数として渡されることができ、関数から戻されることなどができるわけです。こうした関数は、特別な名前と構造を持っています。
定義
関数型の引数 / functional argument ( "Funarg" )とは、その値が関数である引数のことです。
例:
function exampleFunc(funArg) { funArg(); } exampleFunc(function () { alert('funArg'); });
この場合、関数 exampleFunc
に渡される実引数の匿名関数が funarg です。
翻って、 funarg を受け取る関数は、高階関数( higher-order function 略して HOF )と呼ばれます。
こうした関数には、汎関数( functional )、またはより数学的に、作用素( operator )という呼び方もあります。上の例では、関数 exampleFunc
が汎関数、 functional です( "functional" は形から形容詞に見えますが、この場合は名詞です)。
既に述べたように、関数は引数として渡されるだけでなく、他の関数から値として戻されることが可能です。
この、他の関数を戻す関数のことを、関数値をとる関数 / functions with functional value または関数型の関数 / function valued functionsと呼びます。
(function functionValued() { return function () { alert('returned function is called'); }; })()();
通常のデータとして参加できる関数(実引数として渡され、関数型の引数を受け取り、関数値として戻されるなど)は、第一級関数(より一般的に第一級オブジェクト)と呼ばれています。
ECMAScript では、全ての関数が第一級オブジェクトです。
それ自身を引数として受け取る汎関数は、自己適用関数と呼ばれます。
(function selfApplicative(funArg) { if (funArg && funArg === selfApplicative) { alert('self-applicative'); return; } selfApplicative(selfApplicative); })();
自分自身を戻す関数は、自己複製関数と呼ばれます。時折、小説などでは自己増殖という言葉も使われますね。
(function selfReplicative() { return selfReplicative; })();
// 配列を受け入れる // 命令型の関数 function registerModes(modes) { modes.forEach(registerMode, modes); } // 使用例 registerModes(['roster', 'accounts', 'groups']); // 自己複製関数を使った // 宣言型の形式 function modes(mode) { registerMode(mode); // 一つのモードを登録 return modes; // そして関数そのものを戻す } // 使用例:モードを「宣言」する modes ('roster') ('accounts') ('groups')しかしながら、実践的には配列そのものを取り扱う方が、効率的で直観的です。
funarg の中で定義されたローカル変数は、もちろんその funarg のアクティベーション時にアクセスできます。なぜなら、コンテキストに進入する度に、そのコンテキストのデータを保管する変数オブジェクトが作られるからです。
function testFn(funArg) { // funarg のアクティベーション // ローカル変数 localVar にアクセス可能 funArg(10); // 20 funArg(20); // 30 } testFn(function (arg) { var localVar = 10; alert(arg + localVar); });
しかし、ご存じの通り(特に第4章で明らかにしたとおり)、 ECMAScript の関数は親関数で囲われることができ、親コンテキストの変数を利用できます。この機能には、いわゆるfunarg 問題が関係してきます。
Funarg 問題
スタック指向言語においては、関数のローカル変数はスタックに保管されます。そこには、関数のアクティベーションの度に、それらの変数や関数の仮引数が push されていきます。
その関数が return するとき、そうした変数達はスタックから削除されます。このモデルは、関数を関数値として扱う(親関数から戻すような)際に大きな制約となってしまいます。主にこの問題は、関数が自由変数を持つときに現れます。
自由変数とは、関数によって利用されながらも、その関数の引数でも、ローカル変数でも無い変数のことです。
例:
function testFn() { var localVar = 10; function innerFn(innerParam) { alert(innerParam + localVar); } return innerFn; } var someFn = testFn(); someFn(20); // 30
この例では、変数 localVar
が、関数 innerFn
において自由です。
もしここでのシステムが、ローカル変数の保管にスタック指向モデルを採用していたならば、関数 testFn
の return 時にその全ての変数はスタックから削除されてしまっていたでしょう。そうなってしまっては、関数 innerFn
の、外側からのアクティベーション時においてエラーになってしまうでしょう。
さらに、特にこのケース、スタック指向の実装の場合は、関数 innerFn
を戻すこと自体が不可能です。なぜなら innerFn
もまた testFn
においてローカルであり、 testFn
の return 時に削除されるべきものだからです。
関数オブジェクトの別の問題は、動的スコープ実装のシステムにおいて関数を実引数として渡す際に関係します。
例(擬似コード):
var z = 10; function foo() { alert(z); } foo(); // 10 - 静的または動的スコープの両方で (function () { var z = 20; foo(); // 10 - 静的スコープ、 20 - 動的スコープ })(); // foo を実引数として // 渡す際も同様 (function (funArg) { var z = 30; funArg(); // 10 - 静的スコープ、 30 - 動的スコープ })(foo);
動的スコープを採用するシステムでは、変数(識別子)解決は変数の動的な(アクティブな)スタックによって処理されます。従って、自由変数は関数生成時に保存された静的な(レキシカルな)スコープチェーン中ではなく、現在のアクティベーションの動的チェーン中に探索されます。
ここに、曖昧さが現れます。例えばもし今 z
が存在するとき(ローカル変数がスタックから削除される前の例とは異なり)、問題はこうです。関数 foo
の様々な形の呼び出しに際し、 z
にはどの値が使われるべきでしょうか?(どのコンテキストの?。どのスコープの?)。
次のケースが、funarg 問題の二つの種類です。関数から戻される関数値を扱うとき(上方/上向きの funarg )、関数に渡される関数型の引数を扱うとき(下方/下向きの funarg )の二種類です。
こうした問題(及びその亜種)を解決するために、クロージャの考え方が提案されました。
クロージャ
クロージャとは、コードブロックと、そのコードブロックが生成されたコンテキストのデータの組み合わせです。
擬似コードによる例を見てみましょう。
var x = 20; function foo() { alert(x); // 自由変数 "x" == 20 } // foo のクロージャ fooClosure = { call: foo // 関数への参照 lexicalEnvironment: {x: 20} // 自由変数を探索するコンテキスト };
この例の fooClosure
はもちろん擬似コードですが、とはいえ ECMAScript における関数 foo
もまた、その内部プロパティの一つとして自身が生成されたコンテキストのスコープチェーンを持っているのです。
その文脈から想像され得るため、 "lexical"(レキシカル)という言葉はしばしば省略されます。このケースでは、クロージャが生成された際にそのコンテキストのデータが(同時に・その瞬間に)保存される、という点に注目します。このコードブロックが次にアクティベートされるとき、自由変数はそこに共に保存された(閉じ込められた / closured )コンテキストから探索されます。二つ前の例において、 ECMAScript では、変数 z
は必ず 10
と解決されるのです。
定義では「コードブロック」という一般的な概念を用いましたが、これには大抵( ECMAScript においても)「関数」という言葉が使われますし、これを私たちも使っていきます。ただし、全ての実装系においてクロージャが関数のみに結びついているというわけではありません。例えば Ruby プログラミング言語では、クロージャは手続きオブジェクト( proc )、またはラムダ式( lambda )、あるいはコードブロックの形を取り得ます。
実装に関して言うならば、コンテキストが破壊された後もローカル変数を保存するためには、スタックベースの実装はもはや適しません(なぜならそれ自体がスタックベース構造の定義に反するためです)。従って、このようなケースでは、親コンテキストの閉じ込められた( closured )データは動的メモリアロケーション(「ヒープ」、つまりヒープベースの実装)に保存され、ガベージコレクタ( GC )と参照カウントが用いられます。このようなシステムは、スピードの面ではスタックベースのシステムより非効率です。しかしながら、実装は常にこれを最適化していきます。パース時に、関数中で自由変数、関数型の引数、あるいは関数値が使われているか突き止め、それによって、データをスタック、または「ヒープ」のどちらに保存するか決定するのです。
ここまで理論について議論してきましたが、やっとここで ECMAScript に直接関係するクロージャについてお話しします。まず触れておかねばならないことは、 ECMAScript は静的(レキシカル)スコープのみを用いるということです(一方、 Perl など、変数を静的あるいは動的スコープのどちらかを使用するように区別して宣言することができる言語もあります)。
var x = 10; function foo() { alert(x); } (function (funArg) { var x = 20; // funArg にとっての変数 "x" は、 // それが生成された(レキシカル)コンテキストから // 静的に保存されている // 従って funArg(); // 10 、 20 ではなく })(foo);
理論的には、関数を生成した親コンテキストのデータは、その関数の内部 [[Scope]]
プロパティに保存されています。もし関数の [[Scope]]
プロパティの理解について不十分なところがあれば、是非とも Scope プロパティについて詳しくご説明している第4章に戻り、スコープチェーンについて読まれることをお勧めします。全く持って、 Scope とスコープチェーンについてしっかり理解しているならば、 ECMAScript におけるクロージャを理解する際の問題は、自ずと氷解してしまうことと思います。
関数生成のアルゴリズムについてご説明した際、 ECMAScript の全ての関数はクロージャだと述べました。なぜなら、例外なく、全ての関数は、生成時に親コンテキストのスコープチェーンを保存するからです(関数がその後アクティベートされるされないに関わらず、 親のスコープ( [[Scope]]
)は常に、関数生成時にその関数に対して書き込まれます)。
var x = 10; function foo() { alert(x); } // 擬似コード // foo はクロージャ foo: <関数オブジェクト> = { Call:, Scope: [ global: { x: 10 } ], ... // その他のプロパティ };
上でも述べたように、最適化の目的から、関数が自由変数を使わない場合、実装系は親のスコープチェーンを保存しないかもしれません。しかし、 ECMA-262-3 仕様では、これについては何も触れられていません。従って形式上は(技術的なアルゴリズムの観点からは)、全ての関数はその生成時にスコープチェーンを [[Scope]]
プロパティに保存する、と考えることができます。
実装によっては、閉じ込められた( closured )スコープに直接アクセスできるものもあります。例えば Rhino では、変数オブジェクトの章でも議論した、非標準の __parent__
プロパティが関数の [[Scope]]
プロパティに相当します。
var global = this; var x = 10; var foo = (function () { var y = 20; return function () { alert(y); }; })(); foo(); // 20 alert(foo.__parent__.y); // 20 foo.__parent__.y = 30; foo(); // 30 // 最上までスコープチェーン中を移動できる alert(foo.__parent__.__parent__ === global); // true alert(foo.__parent__.__parent__.x); // 10
一つの [[Scope]]
値はみんなのもの
これもまたご説明しておかないとなりません。 ECMAScript における閉じ込められた( closured ) [[Scope]]
は、ある一つの(同じ)レキシカルコンテキストにて生成されたクロージャ間で、同じものが共有されます。これは、あるクロージャの中で閉じ込められた変数を更新した場合、他の(同じコンテキスト中で生成された)クロージャからのこの変数の読み出しに影響するということです。
つまり、全ての内部関数は、同じ親スコープを共有しています。
var firstClosure; var secondClosure; function foo() { var x = 1; firstClosure = function () { return ++x; }; secondClosure = function () { return --x; }; x = 2; // 両方のクロージャの Scope 中にある AO["x"] に影響する。 alert(firstClosure()); // 3 、 firstClosure.Scope から } foo(); alert(firstClosure()); // 4 alert(secondClosure()); // 3
この特徴に関するよく知られたバグがあります。しばしば、ループ中で関数を生成するとき、その時のループカウンタを関数に結びつけようとして(全ての関数がそれ自身の個別に必要な値を保管することを期待して)、プログラマが期待しない結果を得ることがあります。
var data = []; for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3 、 0 ではなく data[1](); // 3 、 1 ではなく data[2](); // 3 、 2 ではなく
前の例がこの振る舞いを説明してくれます。関数群を生成するコンテキストのスコープは、生成される三つ全ての関数に対して、ただ一つです(全ての関数はそのスコープを Scope プロパティを通じて参照します)。つまり親スコープ中の変数 "k" は、簡単に変更され得ます。
図式的には、
activeContext.Scope = [ ... // 高位の変数オブジェクト {data: [...], k: 3} // アクティベーションオブジェクト ]; data[0].Scope === Scope; data[1].Scope === Scope; data[2].Scope === Scope;
この通り、関数のアクティベート時には、最後に代入された "k" の値、つまり 3 が使われるのです。
これは、全ての変数がコード実行より前、つまりコンテキスト進入時に生成されているという事実に関係します。このふるまいは、巻き上げ(未訳)と呼ばれます
追加に囲うコンテキストを作れば、この問題を解決できます。
var data = []; for (var k = 0; k < 3; k++) { data[k] = (function _helper(x) { return function () { alert(x); }; })(k); // "k" の値を渡す } // こうすれば正しい結果に data[0](); // 0 data[1](); // 1 data[2](); // 2
ここで何が起こっているかを見てみましょう。
最初に、関数 _helper
が生成され、直後に実引数 k
を持ってアクティベートされます。
次に、関数 _helper
の戻り値もまた関数であり、まさにこれが、 data
配列の対応する一要素に保存されます。
このテクニックは次のような効果を持ちます。アクティベートされるとき、 _helper
は都度、引数 x
を持った新しいアクティベーションオブジェクトを生成し、 x
の値は渡された変数 k
の値になります。
従って、戻された関数の [[Scope]]
プロパティは次のようになるでしょう。
data[0].Scope === [ ... // 高位の変数オブジェクト 親コンテキストの AO : {data: [...], k: 3}, _helper コンテキストの AO : {x: 0} ]; data[1].Scope === [ ... // 高位の変数オブジェクト 親コンテキストの AO : {data: [...], k: 3}, _helper コンテキストの AO : {x: 1} ]; data[2].Scope === [ ... // 高位の変数オブジェクト 親コンテキストの AO : {data: [...], k: 3}, _helper コンテキストの AO : {x: 2} ];
このようにして、関数の [[Scope]]
プロパティは必要とされる値(追加に生成されたスコープに保管される変数 x
を通じて)への参照を持ちます。
戻された関数からは、引き続きもちろん変数 k
にアクセスすることもできます。全ての関数に対して、正しく 3
となります。
ところで、しばしば JavaScript に関するいくつかの記事において、クロージャを上記のようなパターン、追加の関数の生成に関するものとしてのみ取り扱うような不完全な説明があります。実際的な観点から言えば、このパターンはとても重要なものです。しかしながら、理論的な観点からは、ご説明したとおり、 ECMAScript における全ての関数はクロージャです。
とはいえ、上記のパターンは唯一の解法というわけではありません。変数 "k" の必要な値を得るには、例えば、次のようなアプローチも可能です。
var data = []; for (var k = 0; k < 3; k++) { (data[k] = function () { alert(arguments.callee.x); }).x = k; // "k" を関数のプロパティとして保存する } // この方法でも、全て正しい値を得る data[0](); // 0 data[1](); // 1 data[2](); // 2
Funarg と return
もう一つの特徴は、クロージャからの return
です。 ECMAScript では、クロージャからの return
文は、コントロールフローを呼び出し元コンテキスト( caller )に戻します。他の言語の中には、例えば Ruby においては、異なる方法で return
文を処理するさまざまな形式のクロージャが利用できます。呼び出し元に戻すものもあれば、別のケースでは、アクティブなコンテキストから完全に脱出してしまうことも可能です。
しかし ECMAScript 標準では、 return
の振る舞いは次の通りです。
function getElement() { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // 関数 "forEach" に戻る // しかし getElement からは戻らない alert('found: ' + element); // found: 2 return element; } }); return null; } alert(getElement()); // null 、 2 ではなく
しかし ECMAScript でも、アクティブコンテキストを終了したい場合はある特別な、 "break" 的な例外を throw 、 catch することで実現可能です。
var $break = {}; function getElement() { try { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // getElement から "return" alert('found: ' + element); // found: 2 $break.data = element; throw $break; } }); } catch (e) { if (e == $break) { return $break.data; } } return null; } alert(getElement()); // 2
二つの見方
前述の通り、しばしばプログラマは誤ってクロージャを単なる関数から戻される内部関数であると捉えてしまいます。さらには、ただの匿名関数であると考えてしまう場合もあります。
ここでもう一度強調させてください、全ての関数(その種類に関わらず、匿名であろうと、名前付きであろうと、関数式であろうと、関数定義であろうと)は、スコープチェーンという技術によって、クロージャです。
このルールの例外は、 Function コンストラクタで生成された関数で、この関数の [[Scope]]
はグローバルオブジェクトのみを含みます(訳注:関数の外側の環境を保存していますが、関数の直接の生成元コンテキスト(親コンテキスト)を含まないことから、区別できるものと思います)。
この曖昧さ、問題を明確にするために、 ECMAScript に関するクロージャの、二つの正しい見方を提示させてください。
ECMAScript におけるクロージャとは、
- 理論的な観点からは、全ての関数である。なぜなら全ての関数は生成時に親コンテキストのデータを保存するからである。単純なグローバル関数でさえ、グローバル変数への参照はすなわち自由変数への参照であり、このために一般的なスコープチェーン機構が用いられる。
- 実際的な観点からは、次のような関数に特に関心が注がれる。
- それが生成されたコンテキストが完了した後も存在する(たとえば親関数から返された内部関数)。
- コード中で自由変数を参照する。
実用面では、クロージャは的確で簡潔なデザインをもたらし、 funarg によって定義された条件に基づいてさまざまな計算をカスタマイズすることを可能にします。例として、ソート条件関数を引数として受け取る、配列の sort メソッドを挙げてみましょう。
[1, 2, 3].sort(function (a, b) { ... // ソート条件 });
あるいは、例として、 funarg の条件によって新しい配列を写像する汎関数である、 Array の map
メソッドを見てみましょう。
[1, 2, 3].map(function (element) { return element * 2; }); // [2, 4, 6]
時に、事実上無制限の条件検査を記述した funarg を用いて検索関数を実装することで、便利な手続きとなります。
someCollection.find(function (element) { return element.someProperty == 'searchCondition'; });
さらにまた、適用(する)汎関数、例えば funarg を配列の各要素に適用する forEach
メソッドも挙げられます。
[1, 2, 3].forEach(function (element) { if (element % 2 != 0) { alert(element); } }); // 1, 3
ちなみに、関数オブジェクトの apply
と call
メソッドもまた、関数型プログラミングの適用汎関数に由来します。これら二つのメソッドについては this 値に関する項で既に議論しましたが、ここでは、これらを適用汎関数の役割を果たすものとして見ることにします。関数が、引数に適用されるのです( apply
では引数の配列に対し、 call
では順番に与えられる引数に対して)。
(訳注:この例が分かりづらければ、適用される funarg が主語の位置に来ていて、それを取り扱う汎関数が apply または call であるとして見直してみてください)
(function () { alert([].join.call(arguments, ';')); // 1;2;3 }).apply(this, [1, 2, 3]);
その他のクロージャの重要な応用は、遅延呼び出しです。
var a = 10; setTimeout(function () { alert(a); // 10, after one second }, 1000);
そして、コールバック関数もあります。
... var x = 10; // only for example xmlHttpRequestObject. () { // コールバックは、データの準備ができた後に // 遅れて呼び出される // このコールバックを生成したコンテキストが // 終了していても、変数 "x" は利用できる alert(x); // 10 }; ..
さらにまた、例えば補助的なオブジェクトを隠す目的で、カプセル化されたスコープの生成もあります。
var foo = {}; // 初期化 (function (object) { var x = 10; object.getX = function _getX() { return x; }; })(foo); alert(foo.getX()); // 閉じ込められた( closured ) "x" を得る - 10
結論
この章は、 ECMA-262-3 についてというよりは、一般的な理論についてご説明する方が大くなりました。しかし、この一般的な理論が、 ECMAScript におけるクロージャに関していくつかの面を明らかにし、より理解を深めることになるものと考えます。ご質問があれば、コメントで喜んでお答えします(訳注:はてなダイアリーにも転載する作業、もう少しお待ちください...)。
参考文献
英語版翻訳: Dmitry A. Soshnikov[英語版].
英語版公開日時: 2010-02-28
オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
オリジナルロシア語版公開日時: 2009-07-20
本シリーズはすべて英語版からの訳出です。