EventTarget.addEventListener()

EventTargetaddEventListener() メソッドは、特定のイベントが対象に配信されるたびに呼び出される関数を設定します。 対象としてよくあるものは Element, Document, Window ですが、イベントに対応したあらゆるオブジェクトが対象になることができます (XMLHttpRequest など)。

addEventListener() は関数または EventListener を実装したオブジェクトを、呼び出される EventTarget における指定されたイベント種別のイベントリスナーのリストに加えることで動作します。

構文

target.addEventListener(type, listener [, options]);
target.addEventListener(type, listener [, useCapture]);
target.addEventListener(type, listener [, useCapture, wantsUntrusted  ]); // Gecko/Mozilla only

引数

type
対象とするイベントの種類を表す文字列
listener
指定された型のイベントが発生するときに通知 (Event インターフェースを実装しているオブジェクト) を受け取るオブジェクト。これは、 EventListener インタフェースを実装するオブジェクト、あるいは、単純に、JavaScript の関数でなければなりません。コールバックについて詳しくは、イベントリスナーのコールバックを参照してください。
options Optional
対象のイベントリスナーの特性を指定する、オプションのオブジェクトです。次のオプションが使用できます。
capture
Boolean 値で、この型のイベントが DOM ツリーで下に位置する EventTarget配信 (dispatch) される前に、登録された listener に配信されることを示します。
once
Boolean 値で、 listener の呼び出しを一回のみのとしたいかどうかを値で指定します。 true を指定すると、 listener は一度実行された時に自動的に削除されます。
passive
Boolean 値で、 true ならば、 listener で指定された関数が preventDefault() を呼び出さないことを示します。呼び出されたリスナーが preventDefault() を呼び出すと、ユーザーエージェントは何もせず、コンソールに警告を出力します。詳細はパッシブリスナーによるスクロール性能の改善をご覧ください。
mozSystemGroup
Boolean 値で、リスナーをシステムグループに追加するべきであることを示します。コードが XBL または Firefox ブラウザーの chrome で実行されている場合のみ利用できます。
useCapture Optional
Boolean 値で、この型のイベントが、DOM ツリー内の下の EventTarget に配信される前に、登録された listener に配信されるかどうかを示します。ツリーを上方向にバブリングしているイベントは、キャプチャーを使用するように指定されたリスナーを起動しません。イベントのバブリングとキャプチャーは、両方の要素がそのイベントのハンドラーを登録している場合に、別の要素内に入れ子になっている要素で発生するイベントを伝播する 2 つの方法です。イベント伝播モードは、要素がイベントを受け取る順番を決定します。詳細な説明は DOM Level 3 EventsJavaScript Event order を参照してください。指定されていない場合、 useCapture は既定で false となります。
注: イベントターゲットに登録されたイベントリスナーは、捕捉フェーズやバブリングフェーズではなく、ターゲットフェーズのイベントになります。ターゲットフェーズのイベントは、useCapture 引数にかかわらず、すべてのリスナーを追加された順序で実行します。
注: useCapture はどんなブラウザーでもオプションになっているわけではありません。完全で最大限の互換性を得たいなら、引数を指定するようにしてください。
wantsUntrusted
true の場合、このリスナーはウェブコンテンツによって発火された合成イベント (カスタムイベント) を受け取ります (ブラウザーのクロームでは既定で false ですが、一般のウェブページでは true です)。この引数は Gecko でのみ利用可能であり、主にアドオンやブラウザー自身の役に立つものです。

返値

undefined

使用方法のメモ

イベントリスナーのコールバック

イベントリスナーには、コールバック関数を指定することもできますが、 EventListener を実装したオブジェクトを指定することもでき、その場合は handleEvent() メソッドがコールバック関数として機能します。

コールバック関数自体は、 handleEvent() メソッドと同じ引数と返値を持ちます。つまり、コールバック関数は発生したイベントを説明する Event に基づいたオブジェクトを唯一の引数として受け付け、何も返しません。

たとえば、次のイベントハンドラーコールバックは、 fullscreenchange および fullscreenerror の両方を処理するために使用することができます。

function eventHandler(event) {
  if (event.type == 'fullscreenchange') {
    /* handle a full screen toggle */
  } else /* fullscreenerror */ {
    /* handle a full screen toggle error */
  }
}

オプションの対応の安全な検出

DOM 仕様書の古い版では、 addEventListener() の第 3 引数はキャプチャーを使用するかどうかを示す論理値でした。時間の経過とともに、より多くのオプションが必要であることが明らかになりました。関数にさらに多くの引数を追加する (オプションの値を扱うときに非常に複雑になります) のではなく、第 3 引数は、イベントリスナーを削除する過程を設定するためのオプションの値を定義するさまざまなプロパティを含むことができるオブジェクトに変更されました。

古いブラウザーは (あまり古くないブラウザーも含めて) 第 3 引数がまだ論理であると仮定しているので、このシナリオをインテリジェントに処理できるようにコードを構築する必要があります。これを行うには、興味のあるオプションごとに機能検出を使用します。

例えば、 passive オプションをチェックしたい場合は次のようにします。

let passiveSupported = false;

try {
  const options = {
    get passive() { // この関数はブラウザーが passive プロパティに
                    // アクセスしようとしたときに呼び出されます。
      passiveSupported = true;
      return false;
    }
  };

  window.addEventListener("test", null, options);
  window.removeEventListener("test", null, options);
} catch(err) {
  passiveSupported = false;
}

これは、 options オブジェクトを生成し、 passive プロパティのゲッター関数を持たせます。ゲッターは、呼ばれた場合に passiveSupported フラグを true に設定します。つまり、ブラウザーが passive プロパティの値を options オブジェクトでチェックした場合、 passiveSupportedtrue に設定され、そうでなければ false のままになります。次に addEventListener() を呼び出して、これらのオプションを指定して偽のイベント・ハンドラーをセットアップし、ブラウザーが第 3 引数としてオブジェクトを認識した場合にオプションがチェックされるようにします。その後、 removeEventListener() を呼び出して、自分たちで後始末をします。 (呼ばれていないイベントリスナーでは handleEvent() は無視されることに注意してください。)

この方法で、任意のオプションに対応しているかどうかを確認することができます。上に示したようなコードを使って、そのオプションのゲッターを追加するだけです。

そして、問題のオプションを使用する実際のイベントリスナーを作成したい場合は、次のようにします。

someElement.addEventListener("mouseup", handleMouseUp, passiveSupported
                               ? { passive: true } : false);

ここでは、 mouseup イベントのリスナーを someElement 要素に追加しています。第 3 引数の passiveSupportedtrue である場合、 options オブジェクトを passivetrue に設定して指定しています。そうでない場合は、論理値を渡す必要があることがわかっているので、 useCapture 引数の値として false を渡しています。

ご希望であれば、 ModernizrDetect It のようなサードパーティ製のライブラリを使用してこのテストを行うことができます。

Web Incubator Community GroupEventListenerOptions の記事を参考にしてください。

シンプルなリスナーの追加

この例は、要素上でのマウスクリックを監視するための addEventListener() の使い方を紹介します。

HTML

<table id="outside">
  <tr><td id="t1">one</td></tr>
  <tr><td id="t2">two</td></tr>
</table>

JavaScript

// t2 のコンテンツを変更する関数
function modifyText() {
  const t2 = document.getElementById("t2");
  if (t2.firstChild.nodeValue == "three") {
    t2.firstChild.nodeValue = "two";
  } else {
    t2.firstChild.nodeValue = "three";
  }
}

// イベントリスナーを table に追加
const el = document.getElementById("outside");
el.addEventListener("click", modifyText, false);

このコードの中で、 modifyText()addEventListener() を使用して登録された click イベントのリスナーです。表の中のどこかをクリックすると、ハンドラーに上がり、 modifyText() を実行します。

結果

無名関数を使用したイベントリスナー

ここで、無名関数を使用してイベントリスナーに引数を渡す方法を見てみましょう。

HTML

<table id="outside">
  <tr><td id="t1">one</td></tr>
  <tr><td id="t2">two</td></tr>
</table>

JavaScript

// t2 のコンテンツを変更する関数
function modifyText(new_text) {
  const t2 = document.getElementById("t2");
  t2.firstChild.nodeValue = new_text;    
}
 
// イベントリスナーを table に追加する関数
const el = document.getElementById("outside");
el.addEventListener("click", function(){modifyText("four")}, false);

なお、リスナーは実際にイベントに応答する modifyText() 関数に引数を送信することができるコードをカプセル化している無名関数であることに注意してください。

結果

アロー関数を使用したイベントリスナー

この例はアロー関数表記を使用して実装された、簡単なイベントリスナーを紹介しています。

HTML

<table id="outside">
  <tr><td id="t1">one</td></tr>
  <tr><td id="t2">two</td></tr>
</table>

JavaScript

// t2 の中身を変更するための関数
function modifyText(new_text) {
  const t2 = document.getElementById("t2");
  t2.firstChild.nodeValue = new_text;    
}
 
// アロー関数で table にイベントリスナーを追加
const el = document.getElementById("outside");
el.addEventListener("click", () => { modifyText("four"); }, false);

結果

なお、無名関数とアロー関数は似ており、違いは this のバインドです。無名関数 (及び伝統的なすべての JavaScript 関数) は自身の this を作成するのに対し、アロー関数はそれを含む関数の this を継承します。

つまり、アロー関数を使用したときは、それを含む関数の変数や定数をイベントハンドラーで利用することができます。

Example of options usage

HTML

<div class="outer">
  outer, once & none-once
  <div class="middle" target="_blank">
    middle, capture & none-capture
    <a class="inner1" href="https://www.mozilla.org" target="_blank">
      inner1, passive & preventDefault(which is not allowed)
    </a>
    <a class="inner2" href="https://developer.mozilla.org/" target="_blank">
      inner2, none-passive & preventDefault(not open new page)
    </a>
  </div>
</div>

CSS

.outer, .middle, .inner1, .inner2 {
  display: block;
  width:   520px;
  padding: 15px;
  margin:  15px;
  text-decoration: none;
}
.outer {
  border: 1px solid red;
  color:  red;
}
.middle {
  border: 1px solid green;
  color:  green;
  width:  460px;
}
.inner1, .inner2 {
  border: 1px solid purple;
  color:  purple;
  width:  400px;
}

JavaScript

const outer  = document.querySelector('.outer');
const middle = document.querySelector('.middle');
const inner1 = document.querySelector('.inner1');
const inner2 = document.querySelector('.inner2');

const capture = {
  capture : true
};
const noneCapture = {
  capture : false
};
const once = {
  once : true
};
const noneOnce = {
  once : false
};
const passive = {
  passive : true
};
const nonePassive = {
  passive : false
};

outer.addEventListener('click', onceHandler, once);
outer.addEventListener('click', noneOnceHandler, noneOnce);
middle.addEventListener('click', captureHandler, capture);
middle.addEventListener('click', noneCaptureHandler, noneCapture);
inner1.addEventListener('click', passiveHandler, passive);
inner2.addEventListener('click', nonePassiveHandler, nonePassive);

function onceHandler(event) {
  alert('outer, once');
}
function noneOnceHandler(event) {
  alert('outer, none-once, default');
}
function captureHandler(event) {
  //event.stopImmediatePropagation();
  alert('middle, capture');
}
function noneCaptureHandler(event) {
  alert('middle, none-capture, default');
}
function passiveHandler(event) {
  // Unable to preventDefault inside passive event listener invocation.
  event.preventDefault();
  alert('inner1, passive, open new page');
}
function nonePassiveHandler(event) {
  event.preventDefault();
  //event.stopPropagation();
  alert('inner2, none-passive, default, not open new page');
}

結果

外側、中央、内側のコンテナーをそれぞれクリックして、オプションがどのように動作するかを確認してください。

options オブジェクトで特定の値を使用する前に、ユーザーのブラウザーがそれに対応していることを確認するのが良いでしょう。詳細はオプションの対応の安全な検出を参照してください。

その他の注

なぜ addEventListener() を使うのか

addEventListener() は、 W3C DOM で仕様化されている、イベントリスナーを登録するための方法です。その利点は以下の通りです。

  • イベントに複数のハンドラーを追加することができます。これは、特に、他のライブラリ/拡張で利用しても上手く動作する必要がある AJAX ライブラリや JavaScript モジュール、その他のライブラリや拡張機能と共に動作させる必要があるコードに役立ちます。
  • リスナーがアクティブ化されたときに、その動きを細かく制御することを可能にします (キャプチャリング 対 バブリング)。
  • HTML 要素だけでなく、任意の DOM 要素 で動作します。

別の方法である、イベントリスナーを登録するための古い方法 は、後で説明します。

イベント配信中のリスナーの追加

EventListener がイベント処理中に EventTarget に追加された場合、それが現在のアクションによって実行されることはありませんが、バブリングフェーズのように、後の段階のイベントフローで実行されるかもしれません。

複数の同一のイベントリスナー

複数の同一の EventListener が、同じ EventTarget に同じ引数で登録された場合、重複するインスタンスは反映されません。 EventListener が 2 回呼び出されることはなく、重複するインスタンスは反映されないので、 removeEventListener() で手動で削除する必要はありません。

ただし、無名関数をハンドラーとして使用する場合、そのようなリスナーは同じにはならないことに注意してください。無名関数はループ内であっても繰り返し呼び出されるだけで、同じソースコードを使って定義されていても同じにはならないためです。

ただし、このような場合に同じ名前の関数を繰り返し定義することは、より問題になる可能性があります (後述のメモリの問題を参照してください)。

ハンドラー内での "this" の値

一連の類似した要素に対して一般的なハンドラーを使いたい場合のように、イベントハンドラーが実行される要素を参照したいということがたびたびあります。

addEventListener() を使って要素にハンドラー関数を設定したとき、ハンドラーの中の this の値は要素への参照となります。これはハンドラーに渡された event 引数の currentTarget プロパティの値と同じです。

my_element.addEventListener('click', function (e) {
  console.log(this.className)           // logs the className of my_element
  console.log(e.currentTarget === this) // logs `true`
})

アロー関数は独自の this コンテキストを持たないことをお忘れなく。

my_element.addEventListener('click', (e) => {
  console.log(this.className)           // WARNING: `this` is not `my_element`
  console.log(e.currentTarget === this) // logs `false`
})

イベントハンドラー (例えば onclick) が HTML ソース内の要素に指定されていた場合、属性値の Javascirpt コードは、 addEventListener() を使用するような方法で this の値をバインドしたハンドラー関数に置き換えられます。コード内に this が現れた場合には、要素への参照を表します。

<table id="my_table" onclick="console.log(this.id);"><!-- `this` refers to the table; logs 'my_table' -->
  ...
</table>

this の値は、属性値の中のコードによって呼び出される関数内では、標準的な規則に従って振る舞うことに注意してください。これは次の例で示されています。

<script>
  function logID() { console.log(this.id); }
</script>
<table id="my_table" onclick="logID();"><!-- when called, `this` will refer to the global object -->
  ...
</table>

thislogID() 内においては、グローバルオブジェクト Window (または厳格モードの場合は undefinedになります。

bind() を使用した this の指定

Function.prototype.bind() メソッドで、その関数のすべての呼び出しにおいて this として使用される値を指定できます。これを使えば、関数がどこから呼び出されるかによって this の値が変わってしまうというややこしい問題を簡単に回避できます。ただし、リスナーを後で削除できるように、そのリスナーへの参照を残しておく必要があります。

以下は bind() を使った場合と使わない場合の例です。

const Something = function(element) {
  // |this| is a newly created object
  this.name = 'Something Good';
  this.onclick1 = function(event) {
    console.log(this.name); // this は element なので undefined になります
  };
  this.onclick2 = function(event) {
    console.log(this.name); // this はバインドされた Something オブジェクトなので「Something Good」と出力されます
  };
  element.addEventListener('click', this.onclick1, false);
  element.addEventListener('click', this.onclick2.bind(this), false); // これが仕掛けです
}
const s = new Something(document.body);

上の例の問題は、 bind() の付いたリスナーを削除できないということです。もうひとつの解決策は、あらゆるイベントを捕捉する handleEvent() という特別な関数を使用することです。

const Something = function(element) {
  // |this| is a newly created object
  this.name = 'Something Good';
  this.handleEvent = function(event) {
    console.log(this.name); // this は Something オブジェクトなので「Something Good」と出力されます
    switch(event.type) {
      case 'click':
        // 処理
        break;
      case 'dblclick':
        // 処理
        break;
    }
  };

  // この場合のリスナーは this であって this.handleEvent でないことに注意してください
  element.addEventListener('click', this, false);
  element.addEventListener('dblclick', this, false);

  // リスナーは適切に削除できます
  element.removeEventListener('click', this, false);
  element.removeEventListener('dblclick', this, false);
}
const s = new Something(document.body);

this の参照を扱うためのもう一つの方法は、 EventListener にアクセスする必要のあるフィールドを含むオブジェクトのメソッドを呼び出す関数を渡すことです。

class SomeClass {

  constructor() {
    this.name = 'Something Good';
  }

  register() {
    const that = this;
    window.addEventListener('keydown', function(e) { that.someMethod(e); });
  }

  someMethod(e) {
    console.log(this.name);
    switch(e.keyCode) {
      case 5:
        // some code here...
        break;
      case 6:
        // some code here...
        break;
    }
  }

}

const myObject = new SomeClass();
myObject.register();

イベントリスナーのデータの出し入れ

イベントリスナーは島のようなもので、それらにデータを渡すのは非常に難しく、ましてや実行後にそれらからデータを取り出すのは非常に難しいと思われるかもしれません。イベントリスナーは、イベントオブジェクトという1つの引数を取るだけで、それは自動的にリスナーに渡され、返値が無視されるからです。では、どのようにしてデータを渡したり、取り出したりすることができるのでしょうか?これを行うための良い方法がいくつかあります。

"this" を使用したイベントリスナーへのデータの入力

前述の通り、 Function.prototype.bind() を使用すると this 参照変数を通じてイベントリスナーに値を渡すことができます。

const myButton = document.getElementById('my-button-id');
const someString = 'Data';

myButton.addEventListener('click', function () {
  console.log(this);  // Expected Value: 'Data'
}.bind(someString));

この方法は、イベントリスナーの中からプログラムでイベントリスナーがどの HTML 要素で発生したかを知る必要がない場合に適しています。これを行う主な利点は、実際に引数リストにデータを渡す場合とほぼ同じ方法でイベントリスナーがデータを受け取ることです。

外部スコープのプロパティを使用したイベントリスナーへのデータの入力

外部スコープに (const, let を付けた) 変数宣言が含まれている場合、そのスコープで宣言されたすべての内部関数はその変数にアクセスすることができます(外部関数/内部関数についてはこちらを、変数スコープについてはこちらを参照してください)。したがって、イベントリスナーの外部からデータにアクセスする最も簡単な方法の1つは、イベントリスナーが宣言されているスコープにアクセスできるようにすることです。

const myButton = document.getElementById('my-button-id');
const someString = 'Data';

myButton.addEventListener('click', function() {
  console.log(someString);  // Expected Value: 'Data'

  someString = 'Data Again';
});

console.log(someString);  // Expected Value: 'Data' (will never output 'Data Again')

注: 内側のスコープは外側のスコープにある const, let 変数にアクセスすることができますが、イベントリスナーの定義後に、同じ外側のスコープ内でこれらの変数にアクセスできるようになることは期待できません。なぜでしょうか?単純に、イベントリスナーが実行される頃には、イベントリスナーが定義されたスコープは既に実行を終了しているからです。

オブジェクトを用いたイベントリスナーのデータの出し入れ

JavaScript のほとんどの関数とは異なり、オブジェクトはそのオブジェクトを参照する変数がメモリ内に存在する限り、メモリ内に保持されます。それに加えて、オブジェクトはプロパティを持つことができ、参照によって渡すことができることから、スコープ間でデータを共有するための有力な候補となります。これについて調べてみましょう。

注: JavaScript の関数は厳密にはオブジェクトです。 (そのため、プロパティを持つことができ、メモリ内に永続的に存在する変数に代入されていれば、実行終了後もメモリ内に保持されます。)

オブジェクトを参照する変数がメモリに存在する限り、オブジェクトのプロパティを使用してメモリにデータを格納することができるので、実際にそれらを使用して、イベントリスナーにデータを渡し、イベントハンドラーが実行された後でデータに変更があった場合には、それを戻すことができます。この例を考えてみましょう。

const myButton = document.getElementById('my-button-id');
const someObject = {aProperty: 'Data'};

myButton.addEventListener('click', function() {
  console.log(someObject.aProperty);  // Expected Value: 'Data'

  someObject.aProperty = 'Data Again';  // Change the value
});

window.setInterval(function() {
  if (someObject.aProperty === 'Data Again') {
    console.log('Data Again: True');
    someObject.aProperty = 'Data';  // Reset value to wait for next event execution
  }
}, 5000);

この例では、イベントリスナーとインターバル関数の両方が定義されているスコープは、 someObject.aProperty の元の値が変更される前に実行を終了していたとしても、イベントリスナーとインターバル関数の両方で someObject がメモリ内に (参照によって) 持続するため、両方とも同じデータにアクセスできます (つまり、一方がデータを変更したときに、もう一方がその変更に対応できます)。

注: オブジェクトは参照にで変数に格納されます。つまり、実際のデータのメモリの場所だけが変数に格納されます。とりわけ、これはオブジェクトを「格納」する変数が、実際に同じオブジェクト参照が代入 (「格納」) されている他の変数に影響を与えることができるということです。2つの変数が同じオブジェクトを参照している場合 (例えば、 let a = b = {aProperty: 'Yeah'};)、どちらかから変数のデータを変更すると、もう一方の変数に影響を与えます。

注: オブジェクトは参照によって変数に格納されているので、関数の実行を停止した後も、関数からオブジェクトを返す (データを失わないようにメモリに保存しておく) ことができます。

古い Internet Explorer と attachEvent

IE9 より前の Internet Explorer では、標準の addEventListener ではなく、 attachEvent() を使わなければなりません。 IE に対応するためには、上記の例を以下のように修正しなけれなりません。

if (el.addEventListener) {
  el.addEventListener('click', modifyText, false); 
} else if (el.attachEvent)  {
  el.attachEvent('onclick', modifyText);
}

attachEvent() の欠点が 1 つあります。 this の値がイベントを起こした要素ではなく、 window オブジェクトへの参照になってしまうことです。

attachEvent() メソッドは、ウェブページの特定の要素がサイズ変更されたことを検出するために onresize イベントを対応付けることができました。独自の mselementresize イベントは、イベントハンドラーを登録する addEventListener メソッドによって対応付けられた場合。 onresize と同様の機能を提供し、特定の HTML 要素の寸法が変更されたときに発行されます。

ポリフィル

次のコードをスクリプトの初めに書いておくと、 Internet Explorer 8 では対応していない addEventListener(), removeEventListener(), Event.preventDefault(), Event.stopPropagation() が動作するようになります。このコードは、 handleEvent()DOMContentLoaded イベントにも対応します。

注: useCapture に対応していないため、 IE 8 では代わりの方法はありません。以下のコードは IE 8 への対応を追加するだけです。また、 IE 8 用の代替モジュールは、標準モードのみで動作します。 doctype 宣言が必要です。

(function() {
  if (!Event.prototype.preventDefault) {
    Event.prototype.preventDefault=function() {
      this.returnValue=false;
    };
  }
  if (!Event.prototype.stopPropagation) {
    Event.prototype.stopPropagation=function() {
      this.cancelBubble=true;
    };
  }
  if (!Element.prototype.addEventListener) {
    var eventListeners=[];
    
    var addEventListener=function(type,listener /*, useCapture (will be ignored) */) {
      var self=this;
      var wrapper=function(e) {
        e.target=e.srcElement;
        e.currentTarget=self;
        if (typeof listener.handleEvent != 'undefined') {
          listener.handleEvent(e);
        } else {
          listener.call(self,e);
        }
      };
      if (type=="DOMContentLoaded") {
        var wrapper2=function(e) {
          if (document.readyState=="complete") {
            wrapper(e);
          }
        };
        document.attachEvent("onreadystatechange",wrapper2);
        eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper2});
        
        if (document.readyState=="complete") {
          var e=new Event();
          e.srcElement=window;
          wrapper2(e);
        }
      } else {
        this.attachEvent("on"+type,wrapper);
        eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper});
      }
    };
    var removeEventListener=function(type,listener /*, useCapture (will be ignored) */) {
      var counter=0;
      while (counter<eventListeners.length) {
        var eventListener=eventListeners[counter];
        if (eventListener.object==this && eventListener.type==type && eventListener.listener==listener) {
          if (type=="DOMContentLoaded") {
            this.detachEvent("onreadystatechange",eventListener.wrapper);
          } else {
            this.detachEvent("on"+type,eventListener.wrapper);
          }
          eventListeners.splice(counter, 1);
          break;
        }
        ++counter;
      }
    };
    Element.prototype.addEventListener=addEventListener;
    Element.prototype.removeEventListener=removeEventListener;
    if (HTMLDocument) {
      HTMLDocument.prototype.addEventListener=addEventListener;
      HTMLDocument.prototype.removeEventListener=removeEventListener;
    }
    if (Window) {
      Window.prototype.addEventListener=addEventListener;
      Window.prototype.removeEventListener=removeEventListener;
    }
  }
})();

イベントリスナーを登録するための古い方法

addEventListener() は、DOM 2 Events 仕様で導入されました。それ以前は、以下のようにイベントリスナーを登録していました。

// 関数へのリファレンスを利用する方法—'()' が無いことに注意してください
el.onclick = modifyText;

// 関数式を利用する方法
element.onclick = function() {
   /* ...文... */
};

このメソッドは、要素上に click イベントリスナーが既に存在する場合、置き換えてしまいます。 他のイベント、blur (onblur)、keypress (onkeypress)、などのような関連するイベントハンドラも同様です。

これは本質的に DOM 0 の一部であるため、イベントリスナーを追加するためのこのテクニックは非常に広く対応されており、特別なブラウザー間の互換コードを必要としません。これは、 (IE 8 以前のような) 非常に古いブラウザーに対応しなければならない場合に、イベントリスナーを動的に登録するために使用されます。 addEventListener のブラウザー対応の詳細については、下記の表を参照してください。

メモリに関する問題

const els = document.getElementsByTagName('*');

// ケース 1
for(let i=0 ; i < els.length; i++){
  els[i].addEventListener("click", function(e){/*関数の処理*/}, false);
}

// ケース 2
function processEvent(e){
  /*関数の処理*/
}

for(let i=0 ; i < els.length; i++){
  els[i].addEventListener("click", processEvent, false);
}

上記の最初のケースでは、ループが繰り返されるたびに新しい (無名の) ハンドラー関数が作成されます。 2 番目のケースでは、以前に宣言された同じ関数がイベントハンドラーとして使用され、作成されるハンドラー関数が 1 つしかないため、メモリ消費量が少なくなります。さらに、最初のケースでは、無名関数への参照が保持されないため (ここでは、ループが作成する可能性のある複数の無名関数のいずれも保持されないため)、 removeEventListener() を呼び出すことができません。2つ目のケースでは、 processEvent が関数への参照なので、 myElement.removeEventListener("click", processEvent, false) を実行することが可能です。

実際には、メモリ消費に関しては、関数への参照が保持されていないことが本当の問題ではなく、静的な関数への参照が保持されていないことが問題なのです。以下の問題ケースでは、どちらも関数への参照は保持されていますが、繰り返しのたびに再定義されているため、静的なものではありません。 3 つ目のケースでは、無名関数の参照が反復のたびに再割り当てされています。 4 番目のケースでは、関数の定義全体は不変ですが、 (コンパイラによって[[昇格]]されていない限り) 新しいものとして繰り返し定義されているので、静的ではありません。したがって、単純に[[複数の同一のイベントリスナー]]のように見えますが、どちらの場合も、各反復処理では、ハンドラー関数への独自の参照を持つ新しいリスナーが作成されます。しかし、関数の定義自体は変更されないので、重複するリスナーごとに同じ関数が呼び出される可能性があります (特にコードが最適化されている場合)。

また、どちらの場合も、関数への参照は保持されていたが、追加するごとに繰り返し再定義されていたので、上からの remove 文でリスナーを削除することができますが、最後に追加されたものだけが削除されるようになります。

// For illustration only: Note "MISTAKE" of [j] for [i] thus causing desired events to all attach to SAME element

// ケース 3
for(let i=0, j=0 ; i<els.length ; i++){
  /* do lots of stuff with j */
  els[j].addEventListener("click", processEvent = function(e){/*do something*/}, false);
}

// ケース 4
for(let i=0, j=0 ; i<els.length ; i++){
  /* do lots of stuff with j */
  function processEvent(e){/*do something*/};
  els[j].addEventListener("click", processEvent, false); 
}

パッシブリスナーを用いたスクロールの性能改善

仕様書によれば、 passive オプションの既定値は常に false です。しかし、これは特定のタッチイベントを扱うイベントリスナーが (特に) スクロールを処理しようとしている間にブラウザーのメインスレッドをブロックする可能性をもたらしており、スクロール処理中の性能が大幅に低下する結果になる可能性があります。

この問題を防ぐために、一部のブラウザー (特に Chrome と Firefox) では、文書レベルノードである Window, Document, Document.bodytouchstart および touchmove イベントの passive オプションの既定値を true に変更しています。これにより、イベントリスナーが呼び出されなくなるため、ユーザーがスクロールしている間にページのレンダリングをブロックすることができなくなります。

Note: この変更された動作を実装しているブラウザー (およびそれらのブラウザーのバージョン) を知りたい場合は、下記の互換性一覧表を参照してください。

この動作は下記のように、明示的に passive の値を false に設定することで上書きできます。

/* Feature detection */
let passiveIfSupported = false;

try {
  window.addEventListener("test", null, 
    Object.defineProperty(
      {}, 
      "passive", 
      {
        get: function() { passiveIfSupported = { passive: false }; }
      }
    )
  );
} catch(err) {}

window.addEventListener('scroll', function(event) {
  /* do something */
  // can't use event.preventDefault();
}, passiveIfSupported );

addEventListener()options 引数に対応していない古いブラウザーでは、これを使用しようとすると、機能検出を適切に使用せずに useCapture 引数の使用を防ぐことがあります。

基本的な scroll イベントの passive の値を気にする必要はありません。キャンセルできないので、イベントリスナーはどのような場合でもページのレンダリングをブロックすることはできません。

仕様書

仕様書 状態 備考
DOM
EventTarget.addEventListener() の定義
現行の標準
DOM4
EventTarget.addEventListener() の定義
廃止された
Document Object Model (DOM) Level 2 Events Specification
EventTarget.addEventListener() の定義
廃止された 初回定義

ブラウザーの互換性

Update compatibility data on GitHub
デスクトップモバイル
ChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewAndroid 版 ChromeAndroid 版 FirefoxAndroid 版 OperaiOSのSafariSamsung Internet
addEventListenerChrome 完全対応 1
補足
完全対応 1
補足
補足 Before Chrome 49, the type and listener parameters were optional.
Edge 完全対応 12Firefox 完全対応 1IE 完全対応 9
完全対応 9
未対応 6 — 11
補足 代替名
補足 Older versions of IE supported an equivalent, proprietary EventTarget.attachEvent() method.
代替名 非標準の名前 attachEvent を使用しています。
Opera 完全対応 7Safari 完全対応 1WebView Android 完全対応 1
補足
完全対応 1
補足
補足 Before Chrome 49, the type and listener parameters were optional.
Chrome Android 完全対応 18
補足
完全対応 18
補足
補足 Before Chrome 49, the type and listener parameters were optional.
Firefox Android 完全対応 4Opera Android 完全対応 10.1Safari iOS 完全対応 1Samsung Internet Android 完全対応 1.0
補足
完全対応 1.0
補足
補足 Before Samsung Internet 5.0, the type and listener parameters were optional.
useCapture parameter made optionalChrome 完全対応 1Edge 完全対応 12Firefox 完全対応 6IE 完全対応 9Opera 完全対応 11.6Safari 完全対応 ありWebView Android 完全対応 1Chrome Android 完全対応 18Firefox Android 完全対応 6Opera Android 完全対応 12Safari iOS 完全対応 ありSamsung Internet Android 完全対応 1.0
Form with options object supported (third parameter can be either options or a Boolean, for backwards compatibility)Chrome 完全対応 49Edge 完全対応 ≤18Firefox 完全対応 49IE 未対応 なしOpera 完全対応 ありSafari 完全対応 10WebView Android 完全対応 49Chrome Android 完全対応 49Firefox Android 完全対応 49Opera Android 完全対応 ありSafari iOS 完全対応 10Samsung Internet Android 完全対応 5.0
options: capture optionChrome 完全対応 52Edge 完全対応 ≤18Firefox 完全対応 ありIE 未対応 なしOpera 完全対応 ありSafari 完全対応 ありWebView Android 完全対応 52Chrome Android 完全対応 52Firefox Android 完全対応 ありOpera Android 完全対応 ありSafari iOS 完全対応 ありSamsung Internet Android 完全対応 6.0
options: once optionChrome 完全対応 55Edge 完全対応 ≤18Firefox 完全対応 50IE 未対応 なしOpera 完全対応 42Safari 完全対応 ありWebView Android 完全対応 55Chrome Android 完全対応 55Firefox Android 完全対応 50Opera Android 完全対応 42Safari iOS 完全対応 ありSamsung Internet Android 完全対応 6.0
options: passive optionChrome 完全対応 51Edge 完全対応 ≤18Firefox 完全対応 ありIE 未対応 なしOpera 完全対応 ありSafari 完全対応 ありWebView Android 完全対応 51Chrome Android 完全対応 51Firefox Android 完全対応 ありOpera Android 完全対応 ありSafari iOS 完全対応 ありSamsung Internet Android 完全対応 5.0
options: passive option defaults to true for touchstart and touchmove eventsChrome 完全対応 55Edge 完全対応 79Firefox 完全対応 61IE 未対応 なしOpera ? Safari 未対応 なしWebView Android 完全対応 55Chrome Android 完全対応 55Firefox Android 完全対応 61Opera Android ? Safari iOS 未対応 なしSamsung Internet Android 完全対応 6.0
options: passive option defaults to true for wheel and mousewheel eventsChrome 完全対応 73Edge 完全対応 79Firefox ? IE 未対応 なしOpera ? Safari 未対応 なしWebView Android 完全対応 73Chrome Android 完全対応 73Firefox Android ? Opera Android ? Safari iOS 未対応 なしSamsung Internet Android 完全対応 11.0

凡例

完全対応  
完全対応
未対応  
未対応
実装状況不明  
実装状況不明
実装ノートを参照してください。
実装ノートを参照してください。
非標準の名前を使用しています。
非標準の名前を使用しています。

関連情報