こんにちは、太田です。前回はJavaScriptの基礎的な部分を解説しました。今回はJavaScriptのクロージャについて解説します。クロージャはJavaScriptでは使用頻度が高く(意識して使用していなくとも、ほとんどの場合クロージャが使われています)、今後の連載の中でも積極的に使っていきますのでここで確実に理解してしまいましょう。
クロージャとは
クロージャはその定義を説明されてもなかなか理解できないため、難しいものだと思われがちです。しかし、ソースコードを中心に見方を少し工夫すればすんなりと理解できると思います。
さて、クロージャの前に確認しておくべき基本事項があります。それは、JavaScriptでは関数を入れ子にできる、という点です。ある関数の中に別の関数を定義することができます。基本中の基本ですが、これがクロージャにおいてもっとも重要です。
では、それを踏まえて次のコードを見てください(変数名・関数名に日本語を使っていますが、文字コードにさえ気をつければこのままでも動作します)。
関数Aの中に関数Bを、さらにその中に関数Cを、と関数の定義を入れ子にしてみました。ここで、関数Aのローカル変数である変数aを関数Bと関数Cの内部でも使用しています。
関数Bは関数Aの中に定義された関数(内部関数)なので、同じく関数Aの中で定義された変数aを関数Bの中からも参照することができます。ここまでは「見た目の通り」なので何も複雑なことはないと思います。
さてさて、実はこれが既にクロージャの実例となっています。このように関数の中に定義した内部関数が外側の関数(エンクロージャ)のローカル変数を参照できる仕組みをクロージャと呼んでいます(正確にはもっと小難しい説明になるのですが、JavaScriptではそれだけのことです)。なお内部関数Bから見た変数aはローカル変数でもグローバル変数でもないので、これをレキシカル変数と呼びます。
このように、JavaScriptでは関数を定義した段階でその関数から見える変数が決まっています。それゆえに、関数の中に関数があるとき、内部の関数は外側にある変数を参照できるという見た目の通りの動作をします。
こちらは関数Aを2度呼び出しています。一度目の呼び出しでは2がalertされます。また、2度目の呼び出しでもやはり2がalertされます。関数Bは何度呼び出しても同じ変数aをインクリメントするので、変数aはどんどん値が増えていきますが、一方で関数Aが呼び出されると変数aは初期化されています。当たり前だと思われるかもしれませんが、このことを踏まえて次のソースコードを見てください。
こちらはクロージャと前回取り上げた高階関数との組み合わせです。関数Aは関数Bを返すようにしたので、それを変数βに入れました。変数βはもちろん関数Bなので、変数βを呼び出すと関数Bの定義通り変数aをインクリメントしてからその値を返します。変数β自体は関数Aと関係がありませんが、中身の関数Bは関数Aの内部関数なので、どこからどのように呼び出したとしても関数Bが変数aを参照できることは変わりません。繰り返しますが「関数を定義した段階で」参照できる変数が決まるからです。そのため、変数βを関数として呼び出すたびに変数aがインクリメントされていきます。
一方で、関数Aを再び呼びだして新しい変数γに代入すると、やはり変数γも変数aを0から順番にインクリメントした結果を返します。この結果のとおり、変数βが内部に持つ変数aと変数γが持つ変数aは別物であることがわかると思います。
このあたりは少々ややこしくなってきましたね。とはいえ、関数Aが呼び出されたときに変数aは初期化されるので、関数Aが10回呼び出されたときは変数aも10回初期化されてそれぞれが独立しているのは当然なので、関数Bから参照できる変数aもそれぞれが独立しているのは当然のことです。
ここまででクロージャの解説はほぼ終わりです。ここからは実用的なケースを見ていきます。
テーブルのハイライト
ここで、マウスにあわせてテーブルの列をハイライトする機能を実装してみます。
といっても、これは一部の例外をのぞいてCSSだけで実現することができます。
この通り、CSSの:hover指定だけでほとんどのブラウザで意図通りに動きます。しかし、残念ながらIE6(とIE7、IE8の互換モードなど)ではa要素以外の要素について:hover指定が効かないというバグあるため、IE6用の対応が必要となります(といっても、肝心のコンテンツは表示できているわけですから、この機能がIE6に対応していなくとも利用者はさほど不便には感じないでしょう。従って、あえてIE6対応はしないという選択も考えられます)。
では、ひとまず:hoverの代わりにhoverというクラス名を付加するようにしてみます。
一見上手く動きそうに見えるコードですが、サンプル1のとおり、どの列にマウスを載せても一番下の列がハイライトされてしまいます。ここで、例によって関数ごとに色分けをしてみます。
この通り、onmouseover、onmouseoutはそれぞれ内部関数となっているので、変数trをそれぞれ参照できています。その点は問題ありません。ポイントは、この変数trがonload内のローカル変数であるということです。変数trの宣言がforループのなかにあるので、変数がループの数だけ存在するかのように間違いやすいですが、あくまでtrはonload関数のローカル変数でしかありません。つまり、onload内に変数trはただ1つしか存在しません。必然的にonmouseover、onmouseoutから参照する変数trもその1つだけです。このtrはforループを抜ける際に最後の列(tr)を参照した状態となっているので、onmouseoverでtrのclassを編集するとそれが反映されるので最後の列になります。
ではこの問題を解決するにはforループの中、onmouseoverの外で個別の変数trを作ってあげればよいと考えられます。それを実現してみたのが次のコードです。