これでできる! クロスブラウザJavaScript入門

第5回 JavaScriptの基礎知識#2:クロージャ編

この記事を読むのに必要な時間:およそ 3.5 分

テーブルのハイライト

ここで,マウスにあわせてテーブルの列をハイライトする機能を実装してみます。

といっても,これは一部の例外をのぞいてCSSだけで実現することができます。

:hoverで背景色を変更

tr:hover{
  background-color:#ffffaa;
}
1 2 3
4 5 6
7 8 9

この通り,CSSの:hover指定だけでほとんどのブラウザで意図通りに動きます。しかし,残念ながらIE6(とIE7,IE8の互換モードなど)ではa要素以外の要素について:hover指定が効かないというバグあるため,IE6用の対応が必要となります(といっても,肝心のコンテンツは表示できているわけですから,この機能がIE6に対応していなくとも利用者はさほど不便には感じないでしょう。従って,あえてIE6対応はしないという選択も考えられます⁠⁠。

では,ひとまず:hoverの代わりにhoverというクラス名を付加するようにしてみます。

マウスオーバーで背景色を変える実装(失敗例)

window.onload = function(){
  var table=document.getElementById('js5-table');
  var trs = table.getElementsByTagName('tr');
  for (var i = 0, len=trs.length;i < len; i++){
    var tr = trs[i];
    tr.onmouseover = function(){
      tr.className = 'hover';
    };
    tr.onmouseout = function(){
      tr.className = '';
    };
  }
};

一見上手く動きそうに見えるコードですが,サンプル1のとおり,どの列にマウスを載せても一番下の列がハイライトされてしまいます。ここで,例によって関数ごとに色分けをしてみます。

マウスオーバーで背景色を変える実装(失敗例:関数ごとに色分け)

window.onload = function(){ var table=document.getElementById('js5-table'); var trs = table.getElementsByTagName('tr'); for (var i = 0, len=trs.length;i < len; i++){ var tr = trs[i];
tr.onmouseover = function(){ tr.className = 'hover'; };
tr.onmouseout = function(){ tr.className = ''; };
} };

この通り,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を作ってあげればよいと考えられます。それを実現してみたのが次のコードです。

マウスオーバーで背景色を変える実装(成功例#1)

window.onload=function(){ var table=document.getElementById('js5-table'); var trs = table.getElementsByTagName('tr'); for (var i = 0, len=trs.length;i < len; i++){ var tr = trs[i];
(function(_tr){
_tr.onmouseover = function(){ _tr.className = 'hover'; };
_tr.onmouseout = function(){ _tr.className = ''; };
})(tr);
} }

このようにforループ内に関数を作り,ローカル変数 _tr を用意しました。内部関数であるmouseover/mouseoutの中ではこの_trを参照することができます。このように,クロージャは変数のスコープを切りたい場合などによく利用されます。

クロージャを避ける

さて,折角クロージャを解説しましたが,クロージャは便利な半面メモリを消費する原因(場合によってはメモリリークも)となりますし,パフォーマンスも最善ではありません。パフォーマンスが重要なケースではクロージャに頼らないようにするべきです(といっても,JavaScriptでパフォーマンスが重要になるケースというのはそう多くはないでしょう⁠⁠。

クロージャを避ける方法は単純で,関数をネストしなければよいだけです。例えば今回のケースでは次のようにイベント内のthisがそのイベントの発生しているノードになるという性質を利用して次のように書くこともできます。

マウスオーバーで背景色を変える実装(成功例#2)

for (var i = 0, len=trs.length;i < len; i++){ var tr = trs[i]; tr.onmouseover = function(){ this.className = 'hover'; }; tr.onmouseout = function(){ this.className = ''; }; }

もしくは,イベントをセットする関数を定義しておき,ループ内でその関数を呼び出すことでクロージャを作らずにスコープを切ることができます。

マウスオーバーで背景色を変える実装(成功例#3)

for (var i = 0, len=trs.length;i < len; i++){ mouse_hightlight(trs[i]); } }
function mouse_hightlight(tr){
tr.onmouseover = function(){ tr.className = 'hover'; };
tr.onmouseout = function(){ tr.className = ''; };
}

ただし,この方法はソースの可読性,拡張性でやや劣る面があります。

このように同じ機能を実現する際に色々と工夫ができるところはプログラミングの面白さでもあります。是非,試行錯誤をして工夫してみてください。

最後に,おまけとして1万行のテーブルをハイライトするサンプルを紹介します。1万行はあまり現実的な数字ではないかもしれませんが,そういった大きなテーブルを扱いたい場合でも,工夫次第では十分に実現可能です。

最適化前は初期化が重く,ハイライトもマウスの動きからかなり遅れてしまうなど,実用的ではありません。一方,最適化後は読み込みと表示にやや時間がかかるものの,初期化とハイライトはスムーズです。

最適化の内容は,行ごとの監視からテーブル全体の監視に切り替えて初期化コストをほぼ0に抑え,ハイライト時にclass名の操作ではなく背景色を直接操作にすることで影響範囲を最小化してパフォーマンスを上げる方法を使用しました。詳しい解説は連載が進んでから改めていたします。

まとめ

今回はJavaScriptにおいて重要なクロージャを中心とした関数の扱いを解説しました。次回からはクロスブラウザの最大の難所であるDOMについて紹介しつつ,サンプルコードを解説していきたいと思います。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/