uhyohyo.net

JavaScript初級者から中級者になろう

三章第五回 イベントオブジェクト

イベントオブジェクトというものが存在します。その名の通りイベントに関するオブジェクトで、実はイベントが発生した時はこのオブジェクトを通してイベントの様々な情報を得ることができます。

イベントオブジェクトの取得

では、そのイベントオブジェクトはどうすれば手に入るかというと、実は、addEventListenerで登録したイベントリスナ三章第二回)の第一引数(最初の引数)に、イベントオブジェクトが自動的に渡されます。つまり、

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>test</p>

    <script type="text/javascript">
      var p = document.getElementsByTagName('p').item(0);

      var listener = function(ev){
          //↑これ
      };
      p.addEventListener('click', listener, false);

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

ですね。これを通して、さまざまな情報を得ることができます。

ちなみに、addEventListenerを使わないイベント属性ではどうするかというと、「event」という変数にこれが代入されています。

targetとcurrentTarget

まず、イベントオブジェクトが持つtargetcurrentTargetという2つのプロパティを紹介します。

<!doctype html>
<html>
  <head>
    <title>test</title>
    <style type="text/css">
      div{
        background-color:aqua;
      }
      p{
        background-color:yellow;
      }
    </style>
  </head>
  <body>

    <div>
      <p>test</p>
      <p>test</p>
      <p>test</p>
    </div>

    <script type="text/javascript">
      var listener = function(ev){
          console.log("target : "+ev.target.tagName,"currentTarget : "+ev.currentTarget.tagName);
      };

      document.body.addEventListener('click', listener, false);
    </script>
  </body>
</html>

styleという要素がありますが、これはスタイルシートです。div要素には水色、p要素には黄色の色をつけています。addEventListenerでclickイベントを登録していることが分かるので、いろいろクリックしてみましょう。

前解説したイベントバブリングによって、p要素やdiv要素をクリックしても、body要素に登録したイベントリスナがしっかりと呼び出されていることが分かります。

さて、イベントリスナとしてlistenerという関数を登録していますが、その関数がやっていることは簡単です。第一引数、つまりevがイベントオブジェクトです。やっていることは簡単で、console.logでev.targetが持つプロパティtagNameと、ev.currentTargetが持つプロパティtagNameを表示しているだけです。

tagNameプロパティを持っているということは、targetとcurrentTargetはHTMLElementであることが分かります(HTMLElementは第二章第三回で出てきましたね)。

さて、いろいろ試してみると、currentTarget.tagNameは、常に"BODY"であることがわかります。つまり、currentTargetは、今回、常にbody要素のHTMLElementであったということです。

実は、currentTargetは、そのイベントリスナが登録されている要素を表します。今回の場合、addEventListenerでdocument.body、つまりbody要素にイベントを登録したから、これがBODYであったのです。

targetは、常に同じではないはずです。これは、p要素の部分をクリックしたらtagNameは"P"、div要素の部分をクリックしたらtagNameは"DIV"だったはずです。これはもう分かりますね。

targetは、実際にイベントが起きた要素を表しているのです。だから、今回、p要素をクリックしたらtargetにはp要素のオブジェクトが、div要素をクリックしたらdiv要素のオブジェクトが入っているのです。

このtargetが非常に有用で、親要素に設定したイベントの中で、実際にイベントが起こった要素によって処理を変更したりできるのです。具体的には、例えばP要素がクリックされたときのみログを出したい場合は、次のようにします。

<!doctype html>
<html>
  <head>
    <title>test</title>
    <style type="text/css">
      div{
        background-color:aqua;
      }
      p{
        background-color:yellow;
      }
    </style>
  </head>
  <body>

    <div>
      <p>test</p>
      <p>test</p>
      <p>test</p>
    </div>

    <script type="text/javascript">
      var listener = function(ev){
          if(ev.target.tagName == "P"){
              console.log('p');
          }
      };

      document.body.addEventListener('click', listener, false);
    </script>
  </body>
</html>

このサンプルでは、イベントリスナ自体はbody要素に登録されていますが、イベントオブジェクトのtargetプロパティを調べて、そのtagNameが"P"であるとき、つまりtargetプロパティがp要素であるときのみログを表示しています。

ルートノード

さて、このように、親要素にひとつだけイベントを登録して、そこからtargetなどを利用して処理する方法は、とてもよく使われます。そこで、今回のようにbody要素にイベントを登録してもいいのですが、よりよい方法があります。というもの、できるだけ木構造の上のほうに登録するのがいいのです。

そこで、body要素より上、つまりbody要素の親を考えてみると、思いつくのはhtml要素です。

console.log(document.body.parentNode);

とすると、「HTMLHtmlElement」のように表示されます。

当然、HTMLでは一番上の要素はhtml要素です。ちなみに、このように一番上の要素をルート要素といい、documentが持つdocumentElementプロパティで取得できます。

console.log(document.documentElement.tagName);

とすると、document.documentElementがhtml要素であることが分かります。

さて、それでは、このhtml要素にイベントを登録すればいいのではないかと思います。しかし、なんと違います。次のサンプルを見てみます。

console.log(document.documentElement.parentNode);

ルート要素の、さらに親を表示しようとしています。存在しなければ、nullやundefined(二章第十四回)が表示されるはずです。

しかし、そうではありません。「HTMLDocument」のように表示されると思います。html要素には、さらに親があったのです。じつは、これはdocumentです。次のサンプルを実行するとわかります。

console.log(document.documentElement.parentNode == document);

trueが表示されるはずです。つまり、html要素の親とdocumentが同じであるということです。ちなみに、document.parentNodeを表示するとnullだから、documentが正真正銘の木構造の一番上であることがわかります。

ただ、実際にタグを並べてHTMLを書くときは、明らかにhtml要素が一番上ですよね。documentがhtml要素のさらに親であるのは、DOMの中でのみです。ただ、DOMでは、これを有効に活用します。

html要素の上にさらにdocumentがあったなら、documentも一応ノードの一種だし、documentに直接イベントを登録すればいいのです。つまり、上のサンプルは、

<!doctype html>
<html>
  <head>
    <title>test</title>
    <style type="text/css">
      div{
        background-color:aqua;
      }
      p{
        background-color:yellow;
      }
    </style>
  </head>
  <body>

    <div>
      <p>test</p>
      <p>test</p>
      <p>test</p>
    </div>

      <script type="text/javascript">
        var listener = function(ev){
            if(ev.target.tagName == "P"){
                console.log('p');
            }
        };

        document.addEventListener('click', listener, false);
      </script>
  </body>
</html>

のようにすることができます。

イベントオブジェクトのメソッド

イベントオブジェクトは、プロパティの他にメソッドも持っています。

preventDefault

preventDefaultというメソッドを紹介します。これは、デフォルトアクションを中止するというものです。デフォルトアクションとは、その動作を行ったときに起こるもともとの動作ということです。よく分からないと思うので、次のサンプルを見てみましょう。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p><a href="http://google.com/">test</a></p>

    <script type="text/javascript">
      var listener = function(ev){
          console.log('listener');
      };

      document.getElementsByTagName('a').item(0).addEventListener('click', listener, false);
    </script>
  </body>
</html>

ページのa要素にクリックイベントを登録するだけのサンプルです。イベントリスナは、ログを表示するだけですね。

ここで、その登録先がa要素であるということが問題になります。a要素は、もともとリンクなので、クリックしたらリンク先のページに移動しますね。ここで、「クリック」という動作によって、「リンク先に移動する」という動作がもともと定められているのがわかります。これこそがデフォルトアクションです。

デフォルトアクションがあるのはa要素だけに限りません。たとえば、フォームの送信ボタンを押せばフォームが送信されるのもデフォルトアクションです。

さて、JavaScriptがイベントを登録しても、デフォルトアクションは構わず実行されます。ただし、JavaScriptのイベントのほうがデフォルトアクションより先に実行されます。つまり、上のサンプルでは、a要素をクリックすると、イベントによってログが表示された後に、デフォルトアクションによってリンク先に移動します。

ここで、JavaScriptによるイベントが実行されたあと、別のページに移動しない、つまりデフォルトアクションを中止するのが、preventDefaultです。引数はありません。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p><a href="http://google.com/">test</a></p>
    <script type="text/javascript">
      var listener = function(ev){
          console.log('listener');
          ev.preventDefault();
      };

      document.getElementsByTagName('a').item(0).addEventListener('click', listener, false);
    </script>
  </body>
</html>

こうすると、ログが表示されたあとでpreventDefaultによってデフォルトアクションが中止され、移動しなくなります。このメソッドはかなりよく使うので、ぜひ覚えておきましょう。

stopPropagation

さらに、stopPropagationというメソッドがあります。同じく引数はありません。これは、イベントフローを中断するというものです。これを呼び出すと、イベントフローはその要素で中断し、それ以降先に進まずに終了します。ただ、あくまでイベントフローが終了するだけなので、前述のデフォルトアクションは実行されます。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body onclick="console.log('body');">

    <div onclick="console.log('div');">
      <p onclick="console.log('p');event.stopPropagation();">test</p>
    </div>
  </body>
</html>

今回はaddEventListenerを使わずにイベント属性だけにしてみました。eventという変数は、上で説明した通り、イベント属性の中で直接イベントオブジェクトを表します。

前回説明したイベントバブリングにより、p要素をクリックすると、p→div→bodyの順番で処理されるはずですが、p要素の処理で「p」が表示されたきりで、その後に表示されるはずの「div」「body」は表示されません

これが、まさにp要素でログの後で呼び出しているstopPropagationによるものです。p要素を処理した時点でイベントバブリングが終わってしまうのです。大規模なものになってくると、それより後に別のところでイベントを登録しているかもしれないので、むやみにイベントフローを止めるべきでもないのですが、それでも使うときがあるかもしれません。