JavaScript のイベントハンドラ

本気でやるならonclick属性は避けてライブラリを活用すべき - 帰ってきたHolyGrailとHoryGrailの区別がつかない日記 を読んで,思うところあって書いてみました(決してカウンターアーティクルではない)。

  • むかしむかし JavaScript を触っていた
  • むかしむかしに書かれた JavaScript の本で勉強している/した

人向けに。大元記事(そろそろ本気で学びませんか? | Think IT(シンクイット))の想定読者に近いかなと思います。よって以下は JavaScript の初学者にはまったくおすすめできない(余計な知識がついてしまう)です。

Step 1: はじめのいっぽ

ボタンを押したらメッセージボックスが出現する HTML を書いてみます。

<html><body>
    <script type="text/javascript">

function ShowMessage() {
    alert('Hello, world!');
}

    </script>
    <form>
        <input type="button" value="show message" onclick="ShowMessage()">
    </form>
</body></html>

実直に昔ながらに書いているのでこれがわからないという人はまぁいないと思います。

これぐらいだったら onclick ハンドラに alert() をつっこんじゃうよ,という人もいるでしょうが,のちのちの布石だと思ってください。また,もともとの例だと <a> タグに onclick ハンドラを仕掛けていますが,とりあえず昔ながらの JavaScript で理解しやすく <form> 内の <input type="button"> 要素に仕掛けています。これも今後の布石ということで。

Step 2

このままだと HTML 内に JavaScript ハンドラがまぎれこんでいるので,メンテナンスがしづらいのです。

やっぱり、エンジニアとデザイナーorマークアッパーとの分業の点でHTMLの属性にスクリプトを書いちゃうのはあんまりよろしくない。

たとえばの話だけど関数の名前を変えたかったり、だとか、HTMLを変更したり、っていうときにミスが起こりやすくなってしまう。

本気でやるならonclick属性は避けてライブラリを活用すべき - 帰ってきたHolyGrailとHoryGrailの区別がつかない日記

なので,ハンドラの指定も JavaScript サイドにもっていきたいと思います。

<html><body>
    <form name="form_main">
        <input name="btn_show" type="button" value="show message">
    </form>
    <script type="text/javascript">

function ShowMessage() {
    alert('Hello, world!');
}

document.form_main.btn_show.onclick = ShowMessage;

    </script>
</body></html>

HTML 部分で,さきほどの例と以下の点が違う部分に注目してください。

  • <form> タグに name="form_main" という属性を付与した((document.all.btn_show.onclick = ShowMessage; のようにすると <form> タグに name 属性をつける必要はありませんが,まぁ昔ながらの例ということで))
  • <input> タグに name="btn_show" という属性を付与した

実は

  • <script> ブロックが <form> ブロックより後にきている

という違いもあるのです。気づいた人はなかなかです。

このように <form> 要素や <input> に名前を振ってあれば,

document.form_main.btn_show

のようにして要素を取得することができます*1

この要素の onclick というプロパティが onclick ハンドラを示しているので,ここにハンドラへのポインタを代入してやれば OK なのです。

ん?ハンドラのポインタって何?

初心者がやりがちな間違いとして下記のような記述があります。

document.form_main.btn_show.onclick = ShowMessage();

<input> タグの onclick 属性に指定していた値が「ShowMessage()」だったので,同じように記述してしまいがちです。ですが,このように書くと,btn_show.onclick プロパティに ShowMessage() の「実行結果」が入ってしまいます。ボタンを押してもメッセージボックスが出ません*2

document.form_main.btn_show.onclick = ShowMessage;

と書くと,function ShowMessage という関数,(実行結果ではなく)それ自身,という意味になるので,OK なのです。

<script> ブロックの評価時点

さきほど

  • <script> ブロックが <form> ブロックより後にきている

という違いもあげました。

Step 1 の例と同じく <script> ブロックを <form> ブロックの前に持ってきてみます。

<html><body>
    <script type="text/javascript">

function ShowMessage() {
    alert('Hello, world!');
}

document.form_main.btn_show.onclick = ShowMessage;

    </script>
    <form name="form_main">
        <input name="btn_show" type="button" value="show message">
    </form>
</body></html>

この場合,ボタンを押してもメッセージが出現しません。

ブラウザは <script> ブロックを読み込むと,その時点でブロックを実行しはじめます。

function ShowMessage() ... の部分は,関数を定義しているだけなので問題ありません。この次の document.form_main.btn_show.onclick = ... という行がここの時点で実行されます。しかし,この <script> ブロックを実行している時点では,まだ後半の <form name="form_main"> 以下はまだ読み込まれていません*3。ですので,onclick イベントハンドラを設定できていないのです。


実際,もし,Firefox with Firebug を使っている場合,console に

document.form_main is undefined

というエラーメッセージが(読み込み時に)出力されているはずです。


ピンとこない,という方は,この <script> ブロックの中に document.write() を挿入した場合について想像してみましょう。その内容は <form> の前に出力されますか?後に出力されますか?

Step 3: body onload ハンドラを利用する

じゃあ <script> ブロックは最後にもってこないといけないのか。

そんなことはありません。<input type="button"> 要素に onclick イベントが存在するのと同じように,「ブラウザが HTML 全体を解釈しおえた時点」に発生するイベントが <body> 要素に存在します*4。みなさんお馴染みの <body onload="..."> です。

<html><body onload="WindowOnLoad()">
    <script type="text/javascript">

function ShowMessage() {
    alert('Hello, world!');
}

function WindowOnLoad() {
    document.form_main.btn_show.onclick = ShowMessage;
}

    </script>
    <form name="form_main">
        <input name="btn_show" type="button" value="show message">
    </form>
</body></html>

「ブラウザが HTML 全体を解釈しおえた時点」で <input> タグの onclick ハンドラを設定してあげればよいわけですね。

Step 4: onload ハンドラを分離する

おっと,せっかく HTML 部分から JavaScript ハンドラを分離したのに,<body> タグの中に JavaScript が混入してしまいました。onclick ハンドラと同じように,後付けで設定してみましょう。

<html><body>
    <script type="text/javascript">

window.onload = WindowOnLoad;

function ShowMessage() {
    alert('Hello, world!');
}

function WindowOnLoad() {
    document.form_main.btn_show.onclick = ShowMessage;
}

    </script>
    <form name="form_main">
        <input name="btn_show" type="button" value="show message">
    </form>
</body></html>

あれれ?

  • document.body.onload じゃなくて window.onload なの?((これ実は私も理由がわかりません。おそらく,この <script> ブロックに制御がうつった時点ですでに body onload が実行されているから,時既に遅しなのではないかなぁ,と思います。かといって <head> ブロックに押し込むと今度はまだ body 要素は未存在なのでうまくいかない,と。))
  • function WindowOnLoad() を定義する前に window.onload = WindowOnLoad; を実行しちゃって大丈夫なの?(下記追記を参照するとわかるかも)

などなどの疑問が生じます。が,こういうものだと思ってください。ごめんなさい。JavaScript についてもっともっと勉強していくと理由がわかるかも。

Step 5: よりモダンに DOM model で要素を取得

ここまでで JavaScript と HTML の分離がうまくできました。

ですが,イベントハンドラ設定対象の要素の取得の仕方(document.form_main.btn_show)が,NN2 model & IE4 DHTML model のままです。んー古い。古いこと自体は別にいいですが,要素の name 属性を直接 JavaScript に書き下したりしていて,拡張性・メンテナンス性が低い((IE4 DHTML model なら document.all.item('id') で取得できるのでマシ。))ですね(なんのことやらわからない方は別にわからないままで大丈夫です)。

というわけで,いまどきは,より様々な要素を汎用的に取得できる DOM model で取得します。

<html><body>
    <script type="text/javascript">

window.onload = WindowOnLoad;

function ShowMessage() {
    alert('Hello, world!');
}

function WindowOnLoad() {
    document.getElementById('btn_show').onclick = ShowMessage;
}

    </script>
    <form>
        <input id="btn_show" type="button" value="show message">
    </form>
</body></html>

さきほどの例との違いは,

  • <form> 要素の name 属性がなくなった
  • <input> 要素の「名前」を name 属性ではなく id 属性で指定するようにした
  • 要素の取得方法が,オブジェクト.getElementById(id名) という関数になった

です。つまり,まぁ,関数経由で要素を取得できるようになったわけです。

実際には document.getElementById() というのは長いので,たいてい別名の関数を定義します。

function $(e) {
    return document.getElementById(e);
}

function ShowMessage() {
    alert('Hello, world!');
}

function WindowOnLoad() {
    $('btn_show').onclick = ShowMessage;
}

window.onload = WindowOnLoad;

C をやった人からすると「えっ $ も関数名に指定できるの」と衝撃ものですが,見た目てきにもまぁわかりやすいので慣例的に $() 関数をこのように要素取得関数に当てることが多いです。

無名関数についてちらりと

実は,

function foo(x) {
    alert(x);
}

というのは,

foo
    = function (x) {
        alert(x);
    };

と書くのと同じ意味をもっています(厳密には異なります。下記に追記しました)。後半の「function (x)」という表記,ひっかかりますか?これは無名関数と呼ばれるものです。関数の挙動それ自身を指すのに,別段名前なんか必須じゃないよね,ということです,とさらりとながしておきます。

ともかく,さきほどのプログラムは下記のように書き換えることができます(onload ハンドラ部分だけ書き換えました)。

<html><body>
    <script type="text/javascript">
function $(e) { return document.getElementById(e); }

function ShowMessage() {
    alert('Hello, world!');
}

WindowOnLoad
    = function () {
        $('btn_show').onclick = ShowMessage;
    };

window.onload = WindowOnLoad;

    </script>
    <form>
        <input id="btn_show" type="button" value="show message">
    </form>
</body></html>

WindowOnLoad という function を定義していた部分が,WindowOnLoad という変数に無名関数を代入した形になっています。

このコードをよくよく眺めると,わざわざ WindowOnLoad という変数を媒介させる必要はなさそうです(一度しか使われていませんし)。なので,無名関数を直接代入してみましょう。

function $(e) { return document.getElementById(e); }

function ShowMessage() {
    alert('Hello, world!');
}

window.onload
    = function () {
        $('btn_show').onclick = ShowMessage;
    };

onload ハンドラの記述がややすっきりとしました。また,ハンドラに気の利いた関数名を考える必要がないので楽ですね。この記述で意味がわかりにくい,ということはないでしょう。どちらかというと意味がよりわかりやすくなったのではないでしょうか。


もっともっと無名関数を駆使すると,下記のようになります。

var $ = function (e) { return document.getElementById(e) };

window.onload
    = function () {
        $('btn_show').onclick
            = function () {
                alert('Hello, world!');
            };
    };

相当短くなりました。これが読みやすいという方は,けっこう柔軟な頭の持ち主だと思います。そうではない方も,この後は再び記名 function の記述に戻すのでご安心を。

おまけ: HTML タグ内のイベントハンドラ

Step 1 で出てきた例,

        <input type="button" value="show message" onclick="ShowMessage()">

というのは,JavaScript で書くと,

btn_show.onclick
    = function () { ShowMessage() };

のような記述に相当すると言えます。だから,

        <input type="button" value="show message" onclick="ShowMessage">

と書いても

btn_show.onclick
    = function () { ShowMessage; };

のようになり,ShowMessage() は実行されませんね。

Step 6: スタイルをいじってみる

バカの一つ覚えみたいに Hello, World! といっているのも飽きたし,せっかく $() 関数を定義したのでもうちょっと活用するべく,既存の要素のスタイルを変更するイベントハンドラを書こうと思います。

<html><body>
    <script type="text/javascript">
function $(e) { return document.getElementById(e); }

function ChangeBGColor() {
    $('para_msg').style['backgroundColor'] = '#9999ff';
}

window.onload
    = function () {
        $('btn_color').onclick = ChangeBGColor;
    };

    </script>
    <p id="para_msg">Hello, world!</p>
    <form>
        <input id="btn_color" type="button" value="change color">
    </form>
</body></html>

ボタンをクリックすると「Hello, world!」というメッセージの背景色が変わります。

Step 7: イベントハンドラ多重登録

これで JavaScript におけるイベントハンドラの操作についてマスターできたのでしょうか。

いえいえ。

下記の例を考えてみてください。

<html><body>
    <script type="text/javascript">
function $(e) { return document.getElementById(e); }

function ChangeFGColor() {
    $('para_msg').style['color'] = '#000099';
}

function ChangeBGColor() {
    $('para_msg').style['backgroundColor'] = '#9999ff';
}

window.onload
    = function () {
        $('btn_color').onclick = ChangeBGColor;     // --- (1)

        $('btn_color').onclick = ChangeFGColor;     // --- (2)
    };

    </script>
    <p id="para_msg">Hello, world!</p>
    <form>
        <input id="btn_color" type="button" value="change color">
    </form>
</body></html>

この HTML,どのような挙動を示すでしょうか。

背景色と文字色が変わると思った方,残念でした。(2) で onclick ハンドラの値を上書きしてるんだから文字色だけが変わるに決まってるじゃんと思った方,正解です。ですがちょっと考えてみてください。

あなたはあるチームでコーディングをしているとします。あなたに下った指令はボタンを押すとメッセージの背景色を変更することです。そこで Step 6 のようなコードを書きました。

そこでチームメイトに別の指令が下りました。(同じ)ボタンを押すとメッセージの文字色を変更するように,という指令です。

このような時,上記のようにイベントハンドラの上書きが発生してしまいます。これは困りますね。


下記のようなイベントハンドラ登録関数を書けばこれを回避することはできます。

function $(e) { return document.getElementById(e); }

function add_onclick_event(e, handler) {
    if (e.onclick) {
        var prev = e.onclick;
        e.onclick = function () { prev(); handler(); };
    }
    else
        e.onclick = handler;
}

function ChangeFGColor() {
    $('para_msg').style['color'] = '#000099';
}

function ChangeBGColor() {
    $('para_msg').style['backgroundColor'] = '#9999ff';
}

window.onload
    = function () {
        add_onclick_event($('btn_color'), ChangeBGColor);
        add_onclick_event($('btn_color'), ChangeFGColor);
    };

すでにイベントハンドラが設定されていた場合,新しいイベントハンドラで,その旧ハンドラを呼び出すようにする,というものです。

Step 8: DOM のイベントモデル -- addEventListener()

実はいままで扱ってきた .onclick = のようなプロパティというのは,イベント処理機構の一面しか示していません。内的には,より柔軟なイベント管理メカニズムが存在しています。

そのイベント管理メカニズムを通してイベントを登録する関数が element.addEventListener() です。

<html><body>
    <script type="text/javascript">
function $(e) { return document.getElementById(e); }

function ChangeFGColor() {
    $('para_msg').style['color'] = '#000099';
}

function ChangeBGColor() {
    $('para_msg').style['backgroundColor'] = '#9999ff';
}

window.onload
    = function () {
        var e = $('btn_color');

        e.addEventListener('click', ChangeBGColor, false);
        e.addEventListener('click', ChangeFGColor, false);
    };

    </script>
    <p id="para_msg">Hello, world!</p>
    <form>
        <input id="btn_color" type="button" value="change color">
    </form>
</body></html>

addEventListener() は,「イベントハンドラの差し替え」は行いません。「イベントハンドラの追加」を行うだけです。この例だと,ボタンを押すと ChangeBGColor()ChangeFGColor() の両者が呼ばれます。

また,removeEventListener() 関数でイベントハンドラを削除することもできます。

Step 9: IE でも動くように -- attachEvent()

実は先ほどの addEventListener() 関数は IE では動きません。同じような目的で使う attachEvent() 関数が代わりに用意されています。

<html><body>
    <script type="text/javascript">
function $(e) { return document.getElementById(e); }

function append_event(e, type, handler) {
    if (e.addEventListener)
        e.addEventListener(type, handler, false);
    else
        e.attachEvent('on' + type, handler);
}

function ChangeFGColor() {
    $('para_msg').style['color'] = '#000099';
}

function ChangeBGColor() {
    $('para_msg').style['backgroundColor'] = '#9999ff';
}

window.onload
    = function () {
        var e = $('btn_color');

        append_event(e, 'click', ChangeBGColor);
        append_event(e, 'click', ChangeFGColor);
    };

    </script>
    <p id="para_msg">Hello, world!</p>
    <form>
        <input id="btn_color" type="button" value="change color">
    </form>
</body></html>

ブラウザによって呼ぶ関数が違うので append_event() なるラッパ関数を設けました。attachEvent() の場合,ハンドラ名は onclick のように on- を付けた名称を指定する必要があります。


これで無事いまどきのコーディングでイベントハンドラを書くことができました。


んーと……何か忘れてません?

そう,onload イベントも,ですよね。

<html><body>
    <script type="text/javascript">
function $(e) { return document.getElementById(e); }

function append_event(e, type, handler) {
    if (e.addEventListener)
        e.addEventListener(type, handler, false);
    else
        e.attachEvent('on' + type, handler);
}

function ChangeFGColor() {
    $('para_msg').style['color'] = '#000099';
}

function ChangeBGColor() {
    $('para_msg').style['backgroundColor'] = '#9999ff';
}

append_event(
    window, 'load',
    function () {
        var e = $('btn_color');

        append_event(e, 'click', ChangeBGColor);
        append_event(e, 'click', ChangeFGColor);
    }
);

    </script>
    <p id="para_msg">Hello, world!</p>
    <form>
        <input id="btn_color" type="button" value="change color">
    </form>
</body></html>

おわりに

ブラウザによってイベント処理の実装やインタフェースが異なります*5。今回 append_event() のような関数を作りましたが,既存のライブラリではこのような関数がより汎用的かつより使いやすい形で用意されています。

また,上の例では onload イベントを用いましたが,

一番最初にあげた記事のサンプルには「onload」イベントハンドラでinit関数を実行していますが、これもあまりおすすめしません。

というのも、onloadイベントハンドラはブラウザによって「DOMツリー構築完了」の場合もあれば「画像も含めてHTMLの読み込みがすべて完了後」の場合もあるからです。

現時点では後者の実装の方が多いために、大きい画像が用意されているページでは、画像が読み込み終わるまでに初期化関数が実行されなくなってしまいます。

本気でやるならonclick属性は避けてライブラリを活用すべき - 帰ってきたHolyGrailとHoryGrailの区別がつかない日記

のような問題もあります。既存のライブラリはこういった点も解決しています。

ですので,実際に JavaScript でイベント処理を書くなら既存のライブラリを使った方がよいでしょう。

つけたし

id:HolyGrail さんの記事に「初心者にいきなりライブラリでは……」などの声もいくつかありましたが,私個人の意見としてはいろんな入り口があっていいんじゃないかな,と思います。

せっかくブラウザの差異を吸収してくれるし,『高速道路』としてすばらしいし,いきなりきちんと動くものができたほうが勉強していて楽しいし,実際の現場では既存のライブラリを使うことが多いだろうし。なので初学者はライブラリから入って抽象的な概念をつかんだうえで,より低レイヤな勉強をしたほうがいいんじゃないかなぁと思います。

人によっては,メカニズムから知りたいんだ,という人もいるでしょう(私もわりとそうです)。この記事がそういう方の一助となれば幸いです。まぁメカニズムから,と書きましたが具体的にブラウザの実装や W3C DOM の仕様がどうとかについては踏み込んで勉強していない&書いていないです。なので [http://gihyo.jp/dev/feature/01/firebug:title] に期待シテマス!

2008/05/17 追記

コメント欄でご指摘をうけたので「無名関数」→「匿名関数」に修正しました。ありがとうございました。

2008/05/19 追記

コメント欄ほかでのご指摘によると,言語処理系での一般用語としては「無名関数」のほうがより一般的なようです。Core JavaScript 1.5 Reference 日本語訳 も「無名関数」になっていたので,そちらと合わせる意味でも再び「無名関数」としておきました。id:HolyGrail さんごめんなさい :)。ちなみに英語だと「anonymous function」でガチのようです。

2008/05/19 追記: var f = function (x) { ... } について

2008年05月16日 os0x 丁寧な解説/重箱だけどfunction f(x){}とf=function(x){}は違う。特に後者は=が実行されるまでundefined。/↑細かいけど、これ以外にも前者は関数に名前があるからデバッグ時にウマイとか細かい違いがあるかな。

はてなブックマーク - os0xのブックマーク / 2008年5月16日

まったく気づきませんでした。ご指摘ありがとうございます>id:os0x さん

XXX();      // "XXX" !

YYY();      // ERROR: YYY is not a function

function XXX() {
    alert('XXX');
}

var YYY = function YYY () {
    alert('YYY');
};

YYY();      // "YYY" !

XXX() は関数として前方参照可能なのでエラーになりませんが,YYY() は関数として前方参照不可能((細かいことをいうと前方参照不可能,というわけではありません。JavaScript のスコープの特性として YYY という変数自体はすでに存在していますが,代入文が実行されるまで undefined です。なので,エラーメッセージが「YYY is not defined」ではなく「YYY is not a function」なんですね。))なのでエラーとなります((YYY が微妙に「無名」じゃないのはちょっと意図的。リンク先を勉強するとおもしろいかも))。

JavaScript で関数を定義するのには,下記の3通りの手法があります。

  • function 文」による「関数定義」
  • function 演算子」による「関数式」
  • Function() コンストラク

Function() コンストラクタについては今回は触れていません。XXX() は「関数定義」,YYY は「関数式」になります。「関数定義」の場合,前述のように関数定義(実装)が前方参照可能になるという大きな違いがあります。

ほか,function 文による関数定義には落とし穴があったりします。その他の違いも含めて Core JavaScript 1.5 Reference - Functions日本語訳)の Function constructor vs. function declaration vs. function expression を参照してください*6。日本語訳もあるのでぜひぜひ。

*1:今どきの JavaScript から入った人にとって,おえっという表現だと思いますが,DOM 出現以前はこのように書いていたのです。

*2:もっというと,このままだと HTML を読み込んだ瞬間にメッセージボックスが出現します。なぜそのようになるのかは宿題とします。

*3:厳密には,読み込まれていない,というよりブラウザが解釈していない,ですね

*4:このへん厳密にいうと違います。が,勘弁してください。

*5:一応今回のサンプルコードは Firefox 3 と IE 6 で確認してあります。

*6:本質的には ECMAScript 262 Specification を参照するべきかもしれませんが,大量の PDF ですし文法定義的に書かれているのでめげました。ぐむぅ。