スコープについて理解したところで、本稿最後のテーマとして「クロージャ」について触れておくことにしよう。クロージャとは、ひと言でいうならば、「ローカル変数を参照している関数内関数」のこと。
もっとも、これだけの説明ではなかなかイメージがわきにくいと思うので、具体的にクロージャを利用したコードを1つ挙げてみよう。
function myClosure(init) {
var cnt = init;
return function() {
return ++cnt;
}
}
var result = myClosure(10); [A]
window.alert(result()); // 11 [B]
window.alert(result()); // 12 [C]
window.alert(result()); // 13 [D]
一見すると、myClosure関数は引数initを受け取り、これをインクリメントした結果を返しているように見えるかもしれない。しかし、ここで注目していただきたいのは、myClosure関数が数値ではなく、関数を戻り値として返す高階関数であるという点だ(高階関数については前回の記事も参照)。
通常、myClosure関数の中で定義されたローカル変数cntは、myClosure関数の処理が終了した時点で破棄されるはずであるが、この場合、myClosure関数の戻り値である匿名関数がローカル変数cntを参照している。このため、「myClosure関数の終了後もそのままローカル変数が保持される」というわけだ*3。これで、先ほどの「ローカル変数を参照している関数内関数」という意味が、少しはイメージできてきただろうか。
*3 このような動作も、先ほど紹介した「スコープ・チェーン」の概念を理解していると、大いに納得できるはずだ。ここでは、匿名関数内のCallオブジェクトを先頭に、myClosure関数のCallオブジェクト、Globalオブジェクトというスコープ・チェーンが形成されている。
これが理解できれば、リスト17の後半([B]〜[D])の挙動についても納得できるはずだ。[A]で返された匿名関数は、ローカル変数cntは維持しつつも、これをくくっている関数(ここではmyClosure)とは独立して動作することができる。つまり、[B]では変数resultに格納された匿名関数が呼び出されて、変数cntをインクリメントした結果として「11」を、その後、[C]、[D]でも保持された変数cntを1ずつインクリメントした「12」、「13」を返すことになる。
このように、クロージャは一種の記憶域を提供する仕組みなのである。
■オブジェクトのように振る舞うクロージャ
クロージャを「一種の記憶域を提供する仕組み」として注目してみると、以下のようなコードを記述することもできる。リスト18は、クロージャを定義したmyClosure関数を複数の個所から呼び出し、戻り値を異なる変数に格納した例だ。
function myClosure(init) {
var cnt = init;
return function() {
return ++cnt;
}
}
var result1 = myClosure(1); [A]
var result2 = myClosure(10); [B]
window.alert(result1()); // 2
window.alert(result1()); // 3
window.alert(result2()); // 11
window.alert(result2()); // 12
window.alert(result1()); // 4
一見不可思議な挙動に思われるかもしれないが、スコープ・チェーンの考え方からすれば、当然の挙動でもある。繰り返しであるが、JavaScriptでは関数呼び出しのタイミングで新しいCallオブジェクトを生成する。ここでは、[A]、[B]でそれぞれmyClosure関数が呼び出されたタイミングで、
というスコープ・チェーンが形成されているわけであるが、このスコープ・チェーンがいずれも「実行コンテキストの単位に独立したものである」という点が重要だ。つまり、これらのスコープ・チェーンに属するローカル変数もまた相互に独立したものである(別物である)ということなのである。
これを念頭に置いて、いま一度、リスト18を見れば、コードの流れは明快だ。[A]、[B]でのmyClosure関数呼び出しでそれぞれ独立したクロージャとローカル変数cnt(値は「1」と「10」)を生成する。そして、後続のresult1、result2のクロージャそれぞれの呼び出しでも、独立した値「1」、「10」の変数cntを、それぞれ別物としてインクリメントしていることになる。
このようなクロージャの仕組みは、オブジェクトにおけるプロパティ/メソッドにもよく似ていると思われるかもしれない。実際、[A]、[B]での関数呼び出しはオブジェクトのインスタンス化、クロージャから参照されるローカル変数はプロパティ、クロージャを構成する関数はメソッド、クロージャをくくっている親関数はコンストラクタとなぞらえることができる。
もちろん、クロージャとオブジェクトとが常に置き換え可能であるわけではない。クロージャはその構造上、1つの関数として記述する必要があることから、複雑な処理の定義には不向きである。一方、関数内関数というその構造からシンプルな処理をシンプルに記述するには、オブジェクトよりもクロージャが向いているといえるだろう(オブジェクト定義の構文については、あらためて次回解説の予定だ)。
■イベント・ハンドラにクロージャを適用する
本稿の解説で、クロージャの仕組みと基本的な挙動については理解できたと思う。恐らく、読者諸氏はこれまでも、そして今後もクロージャという言葉を何度も目にすることになるはずであるが、いまいちクロージャという概念が心に染み入ってこないのは、概念そのものの分かりにくさというよりも、クロージャを使用するメリットが見えてこないせいかもしれない。
そこでここでは、クロージャを利用する例として、イベント・ハンドラにクロージャを適用するサンプルを見てみることにしよう。ここで紹介するのは、クリックのたびにキャプションを有効/無効に切り替えるトグル・ボタンを実装するコードだ。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>トグルボタン</title></head>
<body>
<form id="form1" runat="server">
<div>
<input id="btn" type="button" value="無効" />
</div>
</form>
<script type="text/javascript">
document.getElementById('btn').onclick = setButtonState();
function setButtonState() {
var flag = false;
var btn = document.getElementById('btn');
return function() {
flag = !flag;
this.innerText = flag ? "有効" : "無効";
};
}
</script>
</body>
</html>
ここではクロージャを利用して、トグル・ボタンの状態(変数flag)を維持し、これをクリックのたびに反転させているわけだ。
クロージャを利用しない場合、状態はグローバル変数として別個に管理しなければならないが、クロージャを利用することでイベント・ハンドラの中でまとめて記述できるので、すっきりと可読性の高いコードを記述できることが分かるだろう(ここでは、状態を管理する変数がflag1つであるからそうでもないが、管理すべき情報が複数に及んだ場合を想像してほしい)。
また、グローバル変数を利用していないので、もしも同様の機能を持つボタンを設置したいという場合にも、ただ単に、
document.getElementById('btn2').onclick = setButtonState();
のようなハンドラ定義のコードを記述しさえすればよい。
以上、今回はJavaScriptにおける変数のデータ型とスコープ、そして、その派生のトピックとしてクロージャを解説した。次回は、いよいよJavaScriptにおけるオブジェクト指向構文について紹介する予定だ。ご期待いただきたい。
Copyright© Digital Advantage Corp. All Rights Reserved.