イベントへの関数登録について

HTML+JavaScriptで、要素のイベントへの関数登録の方法は、主に以下の3つです。

1. 要素タグのなかに属性(onclick="〜"等)を直接記述する。

【例】
<button onclick="alert('!');">TEST</button>

2. 要素オブジェクトのプロパティへ関数を設定する。

【例】
<button id="test">TEST</button>
<script>
function func(){
  alert("!");
}
var elem = document.getElementById("test");
elem.onclick = func;
</script>

3. 要素オブジェクトのaddEventListenerメソッドまたはattachEventメソッドを使用する。

【例】
<button id="test">TEST</button>
<script>
function func(){
  alert("!");
}
var elem = document.getElementById("test");
if (elem.addEventListener) { //Netscape等
  elem.addEventListener('click', func, false);
} else if (elem.attachEvent) { //IE
  elem.attachEvent('onclick', func);
}
</script>

以下、それぞれについて調べてみます。

1. 要素タグのなかに属性(onclick="〜"等)を直接記述する

このとき、オブジェクトのプロパティをtoStringしてみます。

【例】
<button onclick="alert('!');">TEST</button>
<script>
var elem = document.getElementById("test");
alert(elem.onclick.toString());
</script>
【上記コード実行時のalert文字列】
【IE6】
function anonymous()
{
alert('!');
}
【Netscape7.1, FireFox1.5.0.4】
function onclick(event){
alert("!");
}
【Opera9.0】
function (event){
alert("!");
}

設定したコードをもつ関数が、自動的に作成・登録されているのが分かります。

1-1. イベントオブジェクトの使用

面白いのは、NetscapeFirefoxOperaでは、引数にeventが設定されていることです。これはもちろんイベントオブジェクトです。よって、設定コード内でこの名前(event)によりイベントオブジェクトが使用可能です。

【例】
<button id="test" onclick="alert(event.type);">TEST</button>
「click」と表示される

実は、このコードはIEでも「click」と表示されます。IEでは引数に「event」がないので、「event」は「window.event」とみなされるからです(windowオブジェクトのメンバはグローバル扱い)。

1-2. イベント登録要素の参照

登録した関数は、イベント登録要素のオブジェクトのメンバ(ここではonclick)になるので、イベント登録要素をthisで参照することができます

【例】
<button id="test" onclick="alert(this.id);">TEST</button>
「test」と表示される
<input type="checkbox" onclick="alert(this.checked);"/>
チェック時はtrue、外したらfalse

2. 要素オブジェクトのプロパティへ関数を設定する

ここでも、オブジェクトのプロパティをtoStringしてみます。

【例】
<button id="test">TEST</button>
<script>
function func(){
  alert('!');
}
var elem = document.getElementById("test");
elem.onclick = func;
alert(elem.onclick.toString());	
</script>
【上記コード実行時のalert文字列】
【IE6】
function func(){
  alert('!');
}
【Netscape7.1, FireFox1.5.0.4, Opera9.0】
function func(){
alert("!");
}

そのまんまです^^; Netscape等は空白が削除されたり、「'」が「"」に変換されたりしていますが*1

2-1. イベントオブジェクトの使用

Netscape等は第一引数にイベントオブジェクトが渡されますが、IEでは引数がなく、代わりにwindow.eventを使用します。このため、どちらでも動く関数にするためには、以下のようにします。

【参考】
function func(event){
  event = event || window.event;
  alert(event.type);
}

「A || B」は、「A」がnullでなければ「A」、nullなら「B」になります。

2-2. 引数の指定

イベントに登録する関数には、プログラマが引数を設定することができません。

【参考】これはNG。
<button id="test">TEST</button>
<script>
function func(str){
  alert(str);
}
var elem = document.getElementById("test");
elem.onclick = func("TEST!"); //引数は設定できない
</script>

上記コードを実行すると、ページ読み込み時にいきなり「TEST!」が表示されます。これは「func("TEST!");」が読み込み時に実行されているためです。elem.onclickに登録されるのは「func("TEST!");」の戻り値で、ここではundefinedです。このため、「TEST」ボタンを押しても何も起こりません。

引数を設定したい場合は、以下のように、登録したい関数を呼び出す他の関数を作成し、その関数を登録します

【参考】これはOK。
<button id="test">TEST</button>
<script>
function func(str){
  alert(str);
}
function wrap(){
  func("TEST!");
}
var elem = document.getElementById("test");
elem.onclick = wrap;
</script>

「TEST」ボタンを押すと「TEST!」と表示されます。

上のコードは以下のようにすることもできます。

【参考】上のコードと同等
<button id="test">TEST</button>
<script>
function func(str){
  alert(str);
}
var elem = document.getElementById("test");
elem.onclick = function(){func("TEST!");};
</script>
2-3. 引数指定とイベントオブジェクト使用の併用

関数内でイベントオブジェクトも扱いたい場合は以下のようにします。

【参考】
<button id="test">TEST</button>
<script>
function func(str, event){
  event = event || window.event;
  alert(str + ":" + event.type);
}
function wrap(event){
  func("TEST!", event);
}
var elem = document.getElementById("test");
elem.onclick = wrap;
</script>

「TEST」ボタンを押すと「TEST!/click」と表示されます。

上のコードは以下のようにすることもできます。

【参考】上のコードと同等
<button id="test">TEST</button>
<script>
function func(str, event){
  event = event || window.event;
  alert(str + ":" + event.type);
}
var elem = document.getElementById("test");
elem.onclick = function(event){func("TEST!", event);};
</script>
2-4. イベント登録要素の参照

ここでもイベント登録要素をthisで参照することができます。

【例】
<button id="test">TEST</button>
<script>
function func(){
  alert(this.id); //「test」が表示される
}
var elem = document.getElementById("test");
elem.onclick = func;
</script>
2-5. 複数の関数登録は不可

この方法では、同一要素の同一イベントに登録できる関数は一つだけになります。後から登録すると、前の関数は上書きされてしまいます。

【参考】
<button id="test">TEST</button>
<script>
function func1(){
  alert("TEST1!");
}
function func2(){
  alert("TEST2!");
}
var elem = document.getElementById("test");
elem.onclick = func1;
elem.onclick = func2;
</script>

「TEST」ボタンをクリックすると、「TEST2!」と表示され、「TEST1!」は表示されません。

同一要素の同一イベントに関数を複数登録したい場合は、次の三つ目の方法を使用します。

2-6. 【参考】prototype.jsの使用

2006/10/20変更。へ移動しました。

3. 要素オブジェクトのaddEventListenerメソッドまたはattachEventメソッドを使用する

Netscape等なら要素オブジェクトのaddEventListenerメソッド、IEなら要素オブジェクトのattachEventメソッドを使用します。

ただし、これまでと違い、関数の登録先は要素のプロパティではないです。

<button id="test">TEST</button>
<script>
function func(){
  alert("!");
}
var elem = document.getElementById("test");
if (elem.addEventListener) { //Netscape等
  elem.addEventListener('click', func, false);
} else if (elem.attachEvent) { //IE
  elem.attachEvent('onclick', func);
}
alert(elem.onclick);
</script>

「alert(elem.onclick);」は「null」または「undefined」と表示されます。

登録先は不明。ただし、後述しますが、実行時はIEはwindowオブジェクトのメンバ、IE以外はイベント登録要素オブジェクトのメンバとして実行されます。

3-1. 複数の関数登録が可能

前述したように、同一要素の同一イベントに、関数を複数登録することができます。

【例】
<button id="test">TEST</button>
<script>
function func1(){
  alert("TEST1!");
}
function func2(){
  alert("TEST2!");
}
var elem = document.getElementById("test");
if (elem.addEventListener) { //Netscape等
  elem.addEventListener('click', func1, false);
  elem.addEventListener('click', func2, false);
} else if (elem.attachEvent) { //IE
  elem.attachEvent('onclick', func1);
  elem.attachEvent('onclick', func2);
}
</script>

「TEST」ボタンをクリックすると、alertが二つ続けて表示されます。ここで注意しなければならないのは、IEとその他で実行順序が違うということです。

【上記例実行時のalert表示順序】
【IE】
TEST2! -> TEST1!
【Netscape, Firefox, Opera】
TEST1! -> TEST2!

IEの実行順序はかなり滅茶苦茶です。以下を参照してください。

【参考サイト】Pujolsさんの日記
IEのattachEventについて、「イベント時呼び出し順」は、「設定順」になっていない
3-2. 引数の指定、イベントオブジェクトの使用

前述の「要素オブジェクトのプロパティへ関数を設定する」と同じです。

【例】
<button id="test">TEST</button>
<script>
function func(str, event){
  event = event || window.event;
  alert(str + "/" + event.type);
  //「TEST!/click」と表示される
}
var elem = document.getElementById("test");
if (elem.addEventListener) { //Netscape等
  elem.addEventListener('click', function(event){func("TEST!", event)}, false);
} else if (elem.attachEvent) { //IE
  elem.attachEvent('onclick', function(event){func("TEST!", event)});
}
</script>

(イベントオブジェクトは、attachEventで登録するとIEでも引数で取れるようです。)

3-3. イベント登録要素の参照

IEのattachEventを使用した場合、イベント登録要素オブジェクトをthisで参照できません。

【例】
<button id="test">TEST</button>
<script>
function func(){
  alert(this.id); //IEで「undefined」と表示される。
} 
var elem = document.getElementById("test");
if (elem.addEventListener) { //Netscape等
  elem.addEventListener('click', func, false);
} else if (elem.attachEvent) { //IE
  elem.attachEvent('onclick', func);
}
</script>

attachEventの場合、登録関数はwindowオブジェクトのメンバとして実行されるためです。(IE以外はイベント登録要素オブジェクトのメンバとして実行されています。) この場合は別途要素オブジェクトを取得するしかないようです。Netscape等ではイベントオブジェクトのcurrentTargetプロパティから取得できるのですが・・・。

どうしても、IEでもthisでイベント登録要素オブジェクトを参照したい場合は、call関数かapply関数を使用します。

【例】
<button id="test">TEST</button>
<script>
function func(str, event){
  event = event || window.event;
  alert(str + "/" + event.type + "/" + this.id);
  //「TEST/click/test」と表示される。
} 
var elem = document.getElementById("test");
if (elem.addEventListener) { //Netscape等
  elem.addEventListener('click', func, false);
} else if (elem.attachEvent) { //IE
  elem.attachEvent('onclick',
    function(event){func.call(elem, 'TEST!', event);}); //call関数を使用
    //apply関数なら
    //function(event){func.apply(elem, ['TEST!', event]);});
}
</script>
その他
  • イベントキャプチャとイベントバブル
  • イベントフローの停止(stopPropagationメソッドまたはcancelBubbleプロパティ)
  • イベントキャンセル(preventDefaultメソッドまたはreturnValueプロパティ)
  • イベント関数削除(removeEventListenerメソッドまたはdetachEventメソッド)

等の話題がありますが、ここでは割愛します(逃げたとも言う^^;)。

【参考サイト】
イベントハンドラ
イベント(Event)

また、prototype.jsでの実装方法については、手前味噌ですが以下を参照してください。上記の話題についても参考になるかもしれません。

Eventオブジェクト(に対する拡張)(2) - Backstage of theater.jsの「observeメソッド」
Eventオブジェクト(に対する拡張)(1) - Backstage of theater.jsの「stopメソッド」
Eventオブジェクト(に対する拡張)(3) - Backstage of theater.jsの「stopObservingメソッド」

【参考】prototype.jsのbind、bindAsEventListenerメソッド

(2006/10/20 上記文中の「要素オブジェクトのプロパティへ関数を設定する」から移動)

余談です。prototype.jsではこの場合、Functionクラスの拡張メソッドbindまたはbindAsEventListenerが使えます。

【参考】Function.bindメソッドを使用した場合のイベント関数登録
<button id="test">TEST</button>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
function func(str, event){
  event = event || window.event;
  alert(str + ":" + event.type);
}
var elem = document.getElementById("test");
elem.onclick = func.bind(elem, "TEST!");
</script>

イベント発生時、bindメソッド使用時の第二引数以降(ここでは"TEST!")にイベント発生時の引数(IE以外でイベントオブジェクト)を加えて関数funcが実行されます。

引数指定が必要ないならbindAsEventListenerが使えます。

<button id="test">TEST</button>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
function func(event){
  alert(event.type);
}
var elem = document.getElementById("test");
elem.onclick = func.bindAsEventListener(elem);
</script>

この場合は、関数funcの第一引数eventには自動的に(IEでも)イベントオブジェクトが設定されるので、変換は不要です。

どちらのメソッドも第一引数はイベント登録要素を設定してください。でないと、関数でthisによるイベント登録要素へのアクセスができなくなります・・・。(もちろん、別のオブジェクトのメソッドとして扱いたいのであればそのオブジェクトを設定します)

*1:余談として、「'"!"'」とすると、「"\"!\""」に変換されます