IE でも event.currentTarget を使えるようにする

IE の Event モデルと DOM Events モデルの違い

JavaScript のイベントについて考えてみる。

イベントの規格としてはまず Document Object Model Events がある。 Firefox やら Safari やら Opera やらで実装されている。 んで他方 Internet Explorer は別のモデルを持っている。 ほとんどはメソッド名やプロパティ名が違うだけで差異の吸収はさほど大変ではない、のだが、たまに IE には対応するものが全然なくて困っちゃうことがある。

例えば DOM Events モデルにおける EventTarget.addEventListener メソッド の第 3 引数。 イベント伝播時のどのフェーズでイベントをキャッチするかを指定する引数だが、IE のモデルだとイベントの伝播にキャプチャリングフェーズというのがそもそもないみたいで、IE には対応するものがない。 ただキャプチャリングフェーズでイベントをキャッチしたいという要望はさほど大きくはなさそうだから大問題ってほどでもないという感じ。

でも 「これは欲しいのになぜか IE にはない」 というものがあって、それは Event.currentTarget プロパティ である。 どの要素でイベントをキャッチしたのかを示すこのプロパティであるが、何故か IE の Event オブジェクトにはこれに対応するものがないのである。 イベントの登録時に自明であるから要らない、という考え方なんでしょうか。 謎です。

んで無いのも困るので、IE で currentTarget を取得するためのイベントの登録の仕方を考えてみます。

イベントリスナ内で、this を使って取得する

FirefoxSafari などでは、イベントリスナ内の this が currentTarget と同じオブジェクトをさすようになっています。 IE も this を使って参照できれば悩むことは無いんですが、IE では this を使って参照することもできません。 (IE の場合、イベントリスナ内の this は window をさす、んでしたっけ?)

そこで、IE でもイベントリスナ内で this を使って currentTarget を参照できるようにします。 方法としては、(本当の) イベントリスナの前に関数をかませて、イベントリスナを currentTarget のプロパティとして呼び出す、という方法。

function addListener(elem, type, func) {
if( ! func._bridge ) {
func._bridge = new Array(0);
}
var i = func._bridge.length;
func._bridge[i] = new Array(3);
func._bridge[i][0] = elem;
func._bridge[i][1] = type;
// 橋渡し関数
func._bridge[i][2] = function() {
// elem のプロパティとして func を呼び出す
// => func 内の this は elem を指す
func.apply(elem, arguments);
};
elem.attachEvent("on"+type, func._bridge[i][2]);
};

本当のイベントリスナ func の下に橋渡しおよび管理用のプロパティをつくり、そこに橋渡し関数を登録、橋渡し関数から (本当の) イベントリスナを呼び出す、という構造である。 このとき、イベントリスナの呼び出しに apply メソッドを使っているのがミソである。 上のように登録すると、イベントリスナ func 内の this が elem を指すようになり、無事 currentTarget を取得できる。

ちなみに色々と func._bridge の下についているのは管理用。 イベントリスナを登録するだけで削除することがないなら不要だが、削除するならイベントリスナと橋渡し関数を結びつけるために必要となる。

Event オブジェクトに currentTarget プロパティを追加する

こちらは、考え方自体は上とほとんど同じだけど EventListener に引数として渡される Event オブジェクトに currentTarget プロパティを追加する方法。 DOM Events の仕様的には多分 this で参照するよりは Event.currentTarget を使うほうが正しいと思うので、個人的にはこっちがいいんじゃないのかなぁ、と思ったり。

ちなみにこっちをちょこっと改造すると this での参照も Event.currentTarget プロパティでの参照も両方できるようになると思います。

あと、『初めての JavaScript』 によると attachEvent メソッドでイベントリスナを追加した場合はちゃんと detachEvent メソッドで解除しないとメモリリークしてしまうらしいので、こっちではページ unload 時に自動で detachEvent するようにしています。 おかげでちょっと長くなってしまっていますが。

function addListener(target, type, func) {
// ----- 局所変数の宣言 -----
var i = 0;
var hasBeenAdded = false;
// ----- 処理 -----
// target のプロパティに管理用配列を追加
if( ! target._vividcode_el ) {
target._vividcode_el = new Array(0);
// unload 時に解体
window.attachEvent("onunload", function myself(evt) {
// 配列の中身を null に
for( i = 0; i < target._vividcode_el.length; i++ ) {
target._vividcode_el[i][0] = null;
target._vividcode_el[i][1] = null;
target._vividcode_el[i] = null;
}
// 配列への参照をなくす
target._vividcode_el = null;
// 自分自身を detachEvent
window.detachEvent("onunload", myself);
});
}
// 既に登録済みかどうかチェックする
hasBeenAdded = false;
for( i = 0; i < target._vividcode_el.length; i++ ) {
if( target._vividcode_el[i][0] === func ) {
hasBeenAdded = true;
break;
}
}
// 未登録の場合, 登録する
if( ! hasBeenAdded ) {
i = target._vividcode_el.length;
target._vividcode_el[i] = new Array(func, function(evt) {
// evt.currentTarget を指定
evt.currentTarget = target;
// EventListener 起動
func(evt);
});
}
// addEventListener の方では, 同じ関数を二重に登録しようとすると 2 個目は破棄される.
// attachEvent の方だと 2 個目は破棄されない. 同一の動作になるよう, まず detachEvent する.
target.detachEvent("on"+type, target._vividcode_el[i][1]);
target.attachEvent("on"+type, target._vividcode_el[i][1]);
// unload 時に detachEvent しなければメモリリークを起こすとどこかで読んだので念のため.
// 必要以上に detachEvent する場合もあるが実害はないと思う
// (さすがに動作時間はそんなに変わらないでしょう) ので気にしないことにする.
window.attachEvent("onunload", (function () {
// target._vividcode_el も onunload イベントで解体するので,
// 下手すると参照前に解体されている可能性もある.
// よって, あらかじめ局所変数に読み込んでおく.
var func = target._vividcode_el[i][1];
return function myself(evt) {
target.detachEvent("on"+type, func);
window.detachEvent("onunload", myself);
};
})() );
return true;
}

追記

どうも IE の動作が思ってたのと違っていて上のコードだと問題があることがわかりました。
詳しくは 「名前つきの関数リテラルに関する IE のよくわからない挙動」 をご覧ください。