はじめに

Webページでボタンをクリックしたときにメニューが表示されたり、フォームに入力したときにリアルタイムでバリデーションが行われたりする動的な機能は、JavaScriptの「イベント処理」によって実現されています。

イベント処理を理解することは、インタラクティブなWebアプリケーションを構築するための必須スキルです。本記事では、JavaScriptのイベント処理について以下の内容を初心者向けに具体的なコード例を交えて解説します。

  • イベントとイベントリスナーの基本概念
  • addEventListenerによるイベントの登録方法
  • イベントオブジェクトの使い方
  • イベント伝播(バブリングとキャプチャ)
  • イベント委譲のパターン
  • removeEventListenerによるリスナーの解除

なお、DOM(Document Object Model)の基本概念や要素の取得方法については、JavaScriptのDOMの基本と要素取得の方法で詳しく解説していますので、あわせてご覧ください。

イベントとは

イベントとは、プログラミングしているシステムで発生する「何か」のことです。ブラウザでは、ユーザーの操作やシステムの状態変化などがイベントとして検知され、JavaScriptで反応することができます。

主なイベントの種類

Webブラウザで発生する代表的なイベントには以下のようなものがあります。

カテゴリ イベント名 発生タイミング
マウス click 要素がクリックされたとき
マウス dblclick 要素がダブルクリックされたとき
マウス mouseover マウスポインターが要素に乗ったとき
マウス mouseout マウスポインターが要素から離れたとき
キーボード keydown キーが押されたとき
キーボード keyup キーが離されたとき
フォーム submit フォームが送信されたとき
フォーム input 入力値が変更されたとき
フォーム change 入力値が確定されたとき
フォーム focus 要素にフォーカスが当たったとき
フォーム blur 要素からフォーカスが外れたとき
ウィンドウ load ページの読み込みが完了したとき
ウィンドウ scroll スクロールされたとき
ウィンドウ resize ウィンドウサイズが変更されたとき

イベントリスナーとイベントハンドラー

イベントに反応するには、「イベントリスナー」を設定します。イベントリスナーは特定のイベントの発生を監視し、イベントが発生すると「イベントハンドラー」と呼ばれる関数を実行します。

sequenceDiagram
    participant User as ユーザー
    participant Element as HTML要素
    participant Listener as イベントリスナー
    participant Handler as イベントハンドラー

    User->>Element: クリック
    Element->>Listener: clickイベント発生
    Listener->>Handler: ハンドラー関数を実行
    Handler->>Element: 画面を更新

addEventListenerによるイベントリスナーの登録

イベントリスナーを登録するには、addEventListener()メソッドを使用します。これはMDN Web Docsでも推奨されている方法で、複数のリスナーを追加できるなど多くの利点があります。

基本構文

1
element.addEventListener(イベント名, イベントハンドラー, オプション);
  • イベント名: 待ち受けするイベントの種類("click""keydown"など)
  • イベントハンドラー: イベント発生時に実行する関数
  • オプション: キャプチャの有無などを指定(省略可能)

基本的な使用例

以下は、ボタンをクリックするとアラートを表示する例です。

1
<button id="myButton">クリックしてください</button>
1
2
3
4
5
6
7
// ボタン要素を取得
const button = document.getElementById("myButton");

// クリックイベントのリスナーを登録
button.addEventListener("click", function() {
  alert("ボタンがクリックされました!");
});

名前付き関数を使用する

リスナーを解除する予定がある場合や、同じハンドラーを複数の要素で再利用する場合は、名前付き関数を使用します。

1
2
3
4
5
6
7
8
9
const button = document.getElementById("myButton");

// 名前付き関数でハンドラーを定義
function handleClick() {
  alert("ボタンがクリックされました!");
}

// 名前付き関数をリスナーとして登録
button.addEventListener("click", handleClick);

アロー関数を使用する

ES6以降では、アロー関数を使用してより簡潔に記述できます。

1
2
3
4
5
6
const button = document.getElementById("myButton");

// アロー関数でハンドラーを定義
button.addEventListener("click", () => {
  alert("ボタンがクリックされました!");
});

複数のリスナーを登録する

addEventListener()を使用すると、同じイベントに対して複数のリスナーを登録できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const button = document.getElementById("myButton");

// 1つ目のリスナー
button.addEventListener("click", () => {
  console.log("1つ目のリスナーが実行されました");
});

// 2つ目のリスナー
button.addEventListener("click", () => {
  console.log("2つ目のリスナーが実行されました");
});

// クリックすると両方のリスナーが順番に実行される

イベントオブジェクト

イベントが発生すると、ブラウザはイベントに関する情報を含む「イベントオブジェクト」を自動的に生成し、イベントハンドラーに渡します。

イベントオブジェクトの受け取り

イベントオブジェクトは、イベントハンドラーの第一引数として受け取ります。慣例的にeventevteなどの名前が使われます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const button = document.getElementById("myButton");

button.addEventListener("click", function(event) {
  console.log(event);  // イベントオブジェクトを出力
});

// アロー関数の場合
button.addEventListener("click", (e) => {
  console.log(e);  // イベントオブジェクトを出力
});

よく使うプロパティ

イベントオブジェクトには、イベントに関するさまざまな情報が含まれています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const button = document.getElementById("myButton");

button.addEventListener("click", (e) => {
  // イベントの種類
  console.log(e.type);  // "click"
  
  // イベントが発生した要素
  console.log(e.target);  // <button id="myButton">...</button>
  
  // イベントリスナーが登録されている要素
  console.log(e.currentTarget);  // <button id="myButton">...</button>
  
  // マウスの座標(マウスイベントの場合)
  console.log(e.clientX, e.clientY);  // ビューポート上の座標
  console.log(e.pageX, e.pageY);  // ページ全体での座標
});

targetとcurrentTargetの違い

targetcurrentTargetは似ていますが、イベントがバブリングする場合に異なる値を返します。

1
2
3
<div id="outer">
  <button id="inner">クリック</button>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const outer = document.getElementById("outer");

outer.addEventListener("click", (e) => {
  // target: 実際にクリックされた要素
  console.log("target:", e.target.id);
  
  // currentTarget: リスナーが登録されている要素
  console.log("currentTarget:", e.currentTarget.id);
});

// ボタンをクリックした場合の出力:
// target: inner(実際にクリックされた要素)
// currentTarget: outer(リスナーが登録されている要素)

キーボードイベントのプロパティ

キーボードイベントでは、押されたキーに関する情報を取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const input = document.getElementById("textInput");

input.addEventListener("keydown", (e) => {
  // 押されたキーの値
  console.log(e.key);  // "a", "Enter", "ArrowUp" など
  
  // キーコード(物理的なキーの位置)
  console.log(e.code);  // "KeyA", "Enter", "ArrowUp" など
  
  // 修飾キーの状態
  console.log(e.shiftKey);  // Shiftキーが押されているか
  console.log(e.ctrlKey);   // Ctrlキーが押されているか
  console.log(e.altKey);    // Altキーが押されているか
  console.log(e.metaKey);   // Metaキー(MacのCmd)が押されているか
});

イベントの既定動作を抑制する

一部の要素には、イベント発生時に実行される既定の動作があります。preventDefault()メソッドを使用すると、この既定動作を抑制できます。

フォーム送信の抑制

フォームの送信をJavaScriptで制御したい場合によく使用されます。

1
2
3
4
5
<form id="myForm">
  <input type="text" id="name" required>
  <button type="submit">送信</button>
</form>
<p id="message"></p>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const form = document.getElementById("myForm");
const message = document.getElementById("message");

form.addEventListener("submit", (e) => {
  // フォームの既定の送信動作を抑制
  e.preventDefault();
  
  // 独自の処理を実行
  const name = document.getElementById("name").value;
  if (name.length < 3) {
    message.textContent = "名前は3文字以上で入力してください";
  } else {
    message.textContent = `こんにちは、${name}さん!`;
  }
});

リンクのクリック動作の抑制

リンクのクリック時にページ遷移せず、JavaScriptで処理したい場合に使用します。

1
<a href="https://example.com" id="myLink">リンク</a>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const link = document.getElementById("myLink");

link.addEventListener("click", (e) => {
  // ページ遷移を抑制
  e.preventDefault();
  
  // 独自の処理を実行
  console.log("リンクがクリックされました");
  console.log("遷移先URL:", e.target.href);
});

イベント伝播(バブリングとキャプチャ)

DOM上でイベントが発生すると、そのイベントは親要素へと伝播していきます。この仕組みを理解することは、複雑なUIを構築する上で重要です。

イベント伝播の3つのフェーズ

イベント伝播は以下の3つのフェーズで行われます。

graph TD
    A[document] -->|1. キャプチャフェーズ| B[html]
    B -->|↓| C[body]
    C -->|↓| D[div.outer]
    D -->|↓| E[button.target]
    E -->|2. ターゲットフェーズ| E
    E -->|3. バブリングフェーズ| D
    D -->|↑| C
    C -->|↑| B
    B -->|↑| A
  1. キャプチャフェーズ: documentからイベントターゲットに向かって下降
  2. ターゲットフェーズ: イベントターゲットに到達
  3. バブリングフェーズ: イベントターゲットからdocumentに向かって上昇

バブリングの例

デフォルトでは、イベントはバブリングフェーズで処理されます。

1
2
3
4
5
6
7
<div id="outer" style="padding: 50px; background: lightblue;">
  外側
  <div id="inner" style="padding: 30px; background: lightcoral;">
    内側
    <button id="button">ボタン</button>
  </div>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
const button = document.getElementById("button");

outer.addEventListener("click", () => {
  console.log("outer がクリックされました");
});

inner.addEventListener("click", () => {
  console.log("inner がクリックされました");
});

button.addEventListener("click", () => {
  console.log("button がクリックされました");
});

// ボタンをクリックした場合の出力順序:
// button がクリックされました
// inner がクリックされました
// outer がクリックされました

ボタンをクリックすると、イベントは子要素から親要素へと伝播(バブリング)していきます。

キャプチャフェーズでの処理

addEventListener()の第3引数にtrueまたは{ capture: true }を指定すると、キャプチャフェーズでイベントを処理できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
outer.addEventListener("click", () => {
  console.log("outer(キャプチャ)");
}, true);

inner.addEventListener("click", () => {
  console.log("inner(キャプチャ)");
}, { capture: true });

button.addEventListener("click", () => {
  console.log("button");
});

outer.addEventListener("click", () => {
  console.log("outer(バブリング)");
});

// ボタンをクリックした場合の出力順序:
// outer(キャプチャ)
// inner(キャプチャ)
// button
// outer(バブリング)

イベント伝播の停止

stopPropagation()メソッドを使用すると、イベントの伝播を停止できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
button.addEventListener("click", (e) => {
  console.log("button がクリックされました");
  
  // イベント伝播を停止
  e.stopPropagation();
});

// ボタンをクリックした場合の出力:
// button がクリックされました
// (親要素のリスナーは実行されない)

同じ要素に登録された他のリスナーも含めてすべて停止したい場合は、stopImmediatePropagation()を使用します。

1
2
3
4
5
6
7
8
button.addEventListener("click", (e) => {
  console.log("1つ目のリスナー");
  e.stopImmediatePropagation();
});

button.addEventListener("click", () => {
  console.log("2つ目のリスナー");  // 実行されない
});

イベント委譲

イベント委譲(Event Delegation)は、イベントバブリングの仕組みを活用したパターンです。個々の子要素にリスナーを登録する代わりに、共通の親要素に1つのリスナーを登録することで、効率的にイベントを処理できます。

イベント委譲の利点

  • メモリ効率: 多数の要素に個別のリスナーを登録する必要がない
  • 動的要素への対応: 後から追加された要素にも自動的にイベント処理が適用される
  • コードの簡潔さ: 1つのリスナーで複数の要素を管理できる

基本的な実装例

1
2
3
4
5
6
<ul id="todoList">
  <li>タスク1 <button class="delete">削除</button></li>
  <li>タスク2 <button class="delete">削除</button></li>
  <li>タスク3 <button class="delete">削除</button></li>
</ul>
<button id="addTask">タスクを追加</button>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const todoList = document.getElementById("todoList");
const addButton = document.getElementById("addTask");

// 親要素(ul)に1つのリスナーを登録
todoList.addEventListener("click", (e) => {
  // クリックされた要素が削除ボタンかどうかを確認
  if (e.target.classList.contains("delete")) {
    // 削除ボタンの親要素(li)を削除
    e.target.closest("li").remove();
  }
});

// 新しいタスクを追加
let taskCount = 3;
addButton.addEventListener("click", () => {
  taskCount++;
  const newItem = document.createElement("li");
  newItem.innerHTML = `タスク${taskCount} <button class="delete">削除</button>`;
  todoList.appendChild(newItem);
});

// 後から追加されたタスクも削除可能

closestメソッドを活用する

closest()メソッドを使用すると、クリックされた要素から最も近い親要素を取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<div id="cardContainer">
  <div class="card" data-id="1">
    <h3>カード1</h3>
    <p>説明文</p>
    <button class="edit">編集</button>
    <button class="delete">削除</button>
  </div>
  <div class="card" data-id="2">
    <h3>カード2</h3>
    <p>説明文</p>
    <button class="edit">編集</button>
    <button class="delete">削除</button>
  </div>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const container = document.getElementById("cardContainer");

container.addEventListener("click", (e) => {
  // クリックされたボタンに最も近い.card要素を取得
  const card = e.target.closest(".card");
  
  if (!card) return;  // カード外がクリックされた場合は何もしない
  
  const cardId = card.dataset.id;
  
  if (e.target.classList.contains("edit")) {
    console.log(`カード${cardId}を編集`);
  } else if (e.target.classList.contains("delete")) {
    console.log(`カード${cardId}を削除`);
    card.remove();
  }
});

removeEventListenerによるリスナーの解除

不要になったイベントリスナーは、removeEventListener()メソッドで解除できます。リスナーを解除することで、メモリリークを防ぎ、不要なイベント処理を停止できます。

基本的な使用方法

リスナーを解除するには、addEventListener()で登録したときと同じ関数参照を渡す必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const button = document.getElementById("myButton");

// 名前付き関数でハンドラーを定義
function handleClick() {
  console.log("クリックされました");
}

// リスナーを登録
button.addEventListener("click", handleClick);

// リスナーを解除
button.removeEventListener("click", handleClick);

無名関数では解除できない

無名関数やアロー関数を直接渡した場合、後から解除することはできません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const button = document.getElementById("myButton");

// この方法では解除できない
button.addEventListener("click", () => {
  console.log("クリックされました");
});

// 解除しようとしても、別の関数として扱われるため失敗する
button.removeEventListener("click", () => {
  console.log("クリックされました");
});

onceオプションで自動解除

1回だけ実行したいリスナーは、onceオプションを使用すると便利です。

1
2
3
4
5
6
7
const button = document.getElementById("myButton");

button.addEventListener("click", () => {
  console.log("この処理は1回だけ実行されます");
}, { once: true });

// 1回クリックするとリスナーが自動的に解除される

AbortControllerによる解除

AbortControllerを使用すると、複数のリスナーをまとめて解除できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const button = document.getElementById("myButton");
const controller = new AbortController();

// signalオプションを指定してリスナーを登録
button.addEventListener("click", () => {
  console.log("クリックされました");
}, { signal: controller.signal });

button.addEventListener("mouseover", () => {
  console.log("マウスオーバーされました");
}, { signal: controller.signal });

// 両方のリスナーをまとめて解除
controller.abort();

実践例: インタラクティブなフォームバリデーション

ここまで学んだ内容を活用して、リアルタイムバリデーション付きのフォームを実装してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<form id="registrationForm">
  <div class="form-group">
    <label for="username">ユーザー名(3文字以上)</label>
    <input type="text" id="username" name="username">
    <span class="error" id="usernameError"></span>
  </div>
  
  <div class="form-group">
    <label for="email">メールアドレス</label>
    <input type="email" id="email" name="email">
    <span class="error" id="emailError"></span>
  </div>
  
  <div class="form-group">
    <label for="password">パスワード(8文字以上)</label>
    <input type="password" id="password" name="password">
    <span class="error" id="passwordError"></span>
  </div>
  
  <button type="submit">登録</button>
  <p id="formMessage"></p>
</form>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const form = document.getElementById("registrationForm");
const username = document.getElementById("username");
const email = document.getElementById("email");
const password = document.getElementById("password");
const formMessage = document.getElementById("formMessage");

// バリデーション関数
function validateUsername() {
  const error = document.getElementById("usernameError");
  if (username.value.length < 3) {
    error.textContent = "ユーザー名は3文字以上で入力してください";
    return false;
  }
  error.textContent = "";
  return true;
}

function validateEmail() {
  const error = document.getElementById("emailError");
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailPattern.test(email.value)) {
    error.textContent = "有効なメールアドレスを入力してください";
    return false;
  }
  error.textContent = "";
  return true;
}

function validatePassword() {
  const error = document.getElementById("passwordError");
  if (password.value.length < 8) {
    error.textContent = "パスワードは8文字以上で入力してください";
    return false;
  }
  error.textContent = "";
  return true;
}

// リアルタイムバリデーション(inputイベント)
username.addEventListener("input", validateUsername);
email.addEventListener("input", validateEmail);
password.addEventListener("input", validatePassword);

// フォーカスが外れたときのバリデーション(blurイベント)
username.addEventListener("blur", validateUsername);
email.addEventListener("blur", validateEmail);
password.addEventListener("blur", validatePassword);

// フォーム送信時の処理
form.addEventListener("submit", (e) => {
  e.preventDefault();
  
  const isUsernameValid = validateUsername();
  const isEmailValid = validateEmail();
  const isPasswordValid = validatePassword();
  
  if (isUsernameValid && isEmailValid && isPasswordValid) {
    formMessage.textContent = "登録が完了しました!";
    formMessage.style.color = "green";
  } else {
    formMessage.textContent = "入力内容を確認してください";
    formMessage.style.color = "red";
  }
});

この例では、以下のイベント処理のテクニックを活用しています。

  • inputイベントによるリアルタイムバリデーション
  • blurイベントによるフォーカス離脱時のバリデーション
  • submitイベントとpreventDefault()による送信制御
  • 複数のイベントハンドラーの登録と再利用

まとめ

本記事では、JavaScriptのイベント処理の基本から実践的なパターンまでを解説しました。

  • addEventListener(): イベントリスナーを登録する推奨方法。複数のリスナーを登録可能
  • イベントオブジェクト: targetcurrentTargetpreventDefault()などを活用してイベント情報を取得・制御
  • イベント伝播: バブリングとキャプチャの仕組みを理解し、stopPropagation()で制御可能
  • イベント委譲: 親要素に1つのリスナーを登録することで、効率的に複数の子要素を管理
  • removeEventListener(): 不要なリスナーを解除してメモリ効率を改善

イベント処理はWebアプリケーション開発の基礎となる重要なスキルです。本記事で紹介したパターンを実際のプロジェクトで活用し、インタラクティブなUIを構築してみてください。

参考リンク