uhyohyo.net

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

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

このページの最終更新日:

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

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

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


<!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>

このソースコードでいうと引数evです。これを通して、さまざまな情報を得ることができます。

また、HTML要素のイベント属性(例えばonclick)を利用する場合もイベントオブジェクトが利用可能です。この場合は次の例のように、「event」という変数にイベントオブジェクトが自動的に代入されているので、それを利用します。


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

では、イベントオブジェクトの利用法を見ていきます。

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, "currentTarget:", ev.currentTarget);
      };

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

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

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

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

さて、いろいろ試してみると、ev.currentTargetは常にbody要素(のHTMLElement)であることがわかります。

実は、currentTargetプロパティはそのイベントリスナが登録されている要素を表します。今回の場合、addEventListenerでイベントを登録した対象はdocument.body、すなわちbody要素でした。よって、ev.currentTargetにはbodyが入っていたのです。

一方、ev.targetは常に同じではないはずです。これは、p要素の部分(黄色の部分)をクリックしたらそのp要素が、div要素の部分(水色の部分)をクリックしたらdiv要素が表示されたはずです。これはもう分かりますね。

ev.targetは、実際にイベントが起きた要素(のHTMLElement)を表しているのです。

この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要素にイベントを登録してもいいのですが、これにはひとつ欠点があります。それは、head要素中に記述されたスクリプトでdocument.bodyを参照できないという点です。(HTMLは上から読みこまれるので、head要素の中身が読まれている時点ではまだbody要素は読みこまれておらず、したがってdocument.bodyは存在しないことになります。)

そこで、body要素よりさらに上のhtml要素に登録するというのはひとつの手です。当然、HTML文書では一番上の要素はhtml要素です。ちなみに、このように一番上の要素をルート要素といい、documentが持つdocumentElementプロパティで取得できます。例えば次のようにするとhtml要素が表示されるでしょう。

console.log(document.documentElement);

実は、DOMではhtml要素の上にもさらに親がいます。

console.log(document.documentElement.parentNode);

このようにすると、documentと表示されるはずです。これは実際、今まで触ってきたdocumentです。次のサンプルを実行すると確かめることができます。。

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

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

このように、HTML文書としては木構造の一番上はhtml要素ですが、DOMの世界ではそのさらに上に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>

のようにすることができます。なお、このサンプルではscript要素の中でdocument.bodyを使用していないので、script要素をhead要素内に移動させても動作させることができます。

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

イベントオブジェクトは、プロパティの他にメソッドも持っています。その中でもよく使うものを紹介します。

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要素にクリックイベントを登録するだけのサンプルです。イベントリスナは、ログを表示するだけですね。なお、復習ですが、document.getElementsByTagName('a').item(0)というのはページ内に存在する0番目(最初)のa要素ということです。

a要素というのはリンクでした。リンクというは、クリック(スマートフォン等ではタップですが)したらリンク先のページに移動しますね。つまり、リンクにおいては「クリック」という動作に対して「リンク先に移動する」という動作がもともと定められているのがわかります。これこそがデフォルトアクションです。

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

さて、JavaScriptがイベントを登録しても、デフォルトアクションは構わず実行されます。ただし、JavaScriptのイベントのほうがデフォルトアクションより先に実行されます。つまり、上のサンプルでは、a要素をクリックすると、イベントによってログが表示された後に、デフォルトアクションによってリンク先に移動します。(実際にはページが移動するとコンソールが消去されるため、ログはほとんど見えないでしょう。)

これでは困る場合は多々あります。そこで、デフォルトアクションを中止するメソッド、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要素を処理した時点でイベントバブリングが終わってしまうのです。

一応紹介しましたが、このメソッドは滅多に必要になることはないでしょう。イベントフローを止めるということはまったく別の場所で登録されたイベントハンドラが実行されなくなるかもしれないということであり、明らかにバグの元です。百害あっても一利くらいしかありません。