イベントハンドラの落とし穴 定義編
イベントハンドラとは、イベントが発生した際に実行されるプログラムのこと。ここでいうイベントとは、たとえば「クリックした」や「マウスカーソルが重なった」とか「ページが読み込まれたら」とかのことをいいます。これらをイベントいい、その際に発行されるプログラムをイベントハンドラと言います。
イベントハンドラは、いろいろあるので、ここでは詳しくは記述しませんが、この定義の仕方によってハマることがあるので要注意なのです。
HTMLを含むJavaScriptでは、主に3の定義の仕方があります。それをonloadイベントを例に紹介します。
1.タグの属性として設定する。
これは、最もスタンダードなパターンで、おそらくHTMLの初歩ではこの方法を学ぶはずです。
<body onload="alert('Page loaded')">
2.オブジェクトのプロパティに関数を定義する
私の場合では、次にたどりついたのはこの方法でした。これは、オブジェクトのイベントハンドラプロパティに対し、無名関数を定義する方法です。
window.onload = function() {alert('Page loaded');}
JavaScriptでは変数に対して関数を指定すると、その変数が関数の定義として使用できます。
var $ = function() { return window.document.getElementById(id); } var wrapper = $('wrapper'); // ID属性がwrapperの要素を取得
3.イベントリスナーでイベントを登録する
これは一番かしこい方法といえます。理由は後述します。ですが、IE9以前では実装が異なることがあるので、注意が必要です。
if (window.attachEvent) { // IE8以前 window.attachEvent('onload', function() { alert('Page loaded'); } else { window.addEventListener('load', function() {alert('Page loaded');}, false); }
比較してみる
実際の所どれが一番いいのか。実はこれどれもいいやり方なのですが、決め手となるのはスクリプトの組み方です。
私がハマった箇所としては、必要なものをモジュール単位で準備していって、それぞれで2のやり方でイベントを設定していきました。すると、設定したうちの1つの処理しか実行されませんでした。これはどういうことなのか。
2にもある通り、この方法はwindow.onloadという変数に対して、関数を指定しているに過ぎません。すなわちそれぞれのモジュールがこの方法を採用していたとすると、それぞれがonload変数を上書きしまくって、結局最後に設定した処理しか実行されない用になるのです。
window.onload = function() { alert('Module1'); } window.onload = function() { // onloadイベントを上書きしてしまっている。 alert('Module2'); }
では1はどうなのか。
確かにこれはありですが、私はあまり採用したくありません。理由は、HTMLが複雑になり、処理が紛れてしまう可能性があるからです。ですが、イベントを設定する確実な方法とも言えるので、すべてのスクリプトを完全に管理できるのであれば、採用するに足る方法と思っています。
結局のところ、私は3を採用することが多いです。これは同じイベントに何度設定しても、2のようにすでに登録されたイベントを上書きすることはありません。登録した順に実行してくれます。しかし、IE8以前では、IEとそれ以外では実装が異なるため、考慮が必要です。
IE8以前ではattachEventというビルトインメソッドを使用してイベントを登録します。それ以外ブラウザではaddEventListenerというビルトインメソッドを使用します。
ということでこれらの差分を吸収するための簡単なイベント処理を入れてみましょう。
実装先はloadイベントをよく使うwindowオブジェクトとイベントハンドラをよく設置するElementオブジェクトです。
this.addEvent = function(type, fn) { // 第2引数は必ずfunction if (typeof fn !== 'function') return; // DOMContentLoadedに対応させる if (type == 'domready') type = 'DOMContentLoaded'; var listener = function(e) { // e.targetに発生した要素を設定 e.target = e.srcElement || e.target; fn.call(this, e); } if (this.attachEvent) { // IEへの対応 if (type == 'DOMContentLoaded') type = 'load'; this.attachEvent('on' + type, listener); } else { this.addEventListener(type, listener, false); } } // Elementオブジェクトのprototypeにも設定 Element.prototype.addEvent = this.addEvent
this.addEventはwindow.addEventと同じ意味です。いろいろチェック処理をしていますが重要なのはattachEventとaddEventListenerの箇所とlistener変数にfunctionを代入している箇所です。
IEとその他のブラウザではイベントの発生源の取得方法が違うため、それを統一しています。
使い方は以下のとおりです。
window.addEvent('domready', function(e) { alert('Content loaded!'); document.getElementById('button').addEvent('click', function(e) { alert('Button clicked!'); }); });
このようにイベントというのはブラウザ依存や実装に若干注意が必要なので、気をつけましょう。