Javascriptの基本:クロージャー(closure)とその読み方
よく、下のような形式の関数を『クロージャー(closure)』と呼ぶ。
// クロージャー var test1 = function(){ --- (1) var _i = 1; return function(){ alert(_i); _i++; }; };
形式的には、先のログ『Javascriptの基本:いろいろな関数』で照会した、「関数を戻り値にする関数」と同じ。
これを以下のようにして実行すると、'1'、'2’…という値がアラートに上がってくる。
var func = test1(); func(); // '1'がアラートされる。 func(); // '2'がアラートされる。 func(); // '3'がアラートされる。
perlやruby,phpなどでもクロージャー(と呼ばれるもの)を作ることができる(Java7へのクロージャーの導入は見送られた)。
クロージャーと聞いて「やっかいだな」と思ってしまうのは、これが、「ラムダ関数」であるとか、難しげなものを交えて説明されていること。簡単に言い切ってしまうと、
(関数系言語において)「状態を持つ関数」をカプセル化する方法
と見ればよい。
ここでいう「状態」とは、上の関数(1)の変数_iのようなもののこと。javaでいうプロパティーと考えればいい。
このクロージャー、(関数系言語ではなく)オブジェクト指向言語としてJavascriptを捉えた場合、(javaでいうところの)スタティックなオブジェクト、とも見えなくもない。(関数が関数を内包しているので、内部クラスを持つ、スタティッククラスといったところか)
ともあれ、このクロージャーを「どう読むか」というのは一つの課題。
先のログ『Javascriptの基本:いろいろな関数』の復習も兼ねて紐解いてみる。
まず、説明のために、上に書いた2つのコードを一つにまとめておく。
// クロージャー var test1 = function(){ var _i = 1; // ---(4) return function(){ alert(_i); _i++; // ---(5) }; // --- (1) }; var func = test1(); // --- (2) func(); // '1'がアラートされる。 --- (3) func(); // '2'がアラートされる。 func(); // '3'がアラートされる。
クロージャーを特徴付けているのは、return文で指定された関数(1)であるが、まず、(2)の代入式が
test1を実行した結果が、funcとラベル付けされる
であることに注意する。ここで、「test1を実行した結果」とは、(4)式の実行とreturn文の実行であるが、return文で指定された無名関数(1)は(当然だが)実行されない。
とすると、このとき何が戻るの? というと、
戻り値は、無名関数(1)への参照
ということになり、それが、funcと名づけられる。
となれば、(3)式は
無名関数(1)の実行
を表すことになる。このとき、
無名関数(1)からは、スコープチェーンによってtest1のローカル変数_iへのアクセスが可能である
から、(5)によって、_iがカウントアップしていく。
「ローカル変数が状態を保持する」という面白い仕組みも、javascriptではスコープチェーンの機能で実現される。この辺りの詳細については、ZDNet:白川俊平氏の記事『JavaScriptクロージャを完全理解!スコープチェインを知る(後編)』に詳しい。
これによれば、ローカル変数のアロケートは関数の呼び出し時に行われる(アクティベーション・オブジェクトというらしい)。
したがって、_i=0は(2)式で生成される。(3)式以降はスコープチェーンにより_iにアクセスするため、(_iの初期化は行われず)_iがインクリメントする。
これが、クロージャーの読解。
また、先のログで「即時実行の関数」を照会した。
(function(){ alert(_i); _i++; })();
この関数を使って、先ほどの例を以下のように書き直してみると面白い。
var test1 = function(){ // コンストラクタ var _i = 1; ---(1) return (function(){ alert(_i); _i++; })(); // --- (3) }; var func = test1; --- (2) func(); // --- (4) func();
なんとなく、クロージャー風になった。
ここでの注目は(2)式で、test1の実行結果ではなく、test1の参照をfuncに代入してる。
これを、
var func = test1(); --- (2)'
としてしまうと、test1が実行されたときに、return文で指定された関数(3)も実行されてしまい、1がもどってきてしまう。
そうすると、(4)は無効(パースエラー)になってしまう。
だから、(2)式のようにかくのだが、そのようにしても、(4)式は「test1の実行」となって必ず1を戻す。したがって、func();はずっと1のままとなる(つまり、状態を保持しなくなってしまう)。