本文へスキップ
JavaScript 基礎 JavaScript イベント委譲 バブリング

イベントの伝播とイベント委譲を理解しよう

更新日:2026-08-02

子要素で発生したイベントは親要素へ伝わります。この仕組みを利用すると、親へ一つのイベントを登録するだけで複数要素を扱えます。

今回は、記事一覧の「詳細」と「削除」を一つの親要素で受け取ります。イベントがどこからどこへ伝わるかをConsoleで確認してから、イベント委譲を実装します。

今回使用するファイル構成

javascript-practice/
├── index.html
└── js/
    └── script.js

イベント委譲を実装する流れ

バブリングを確認する

<div id="article-list">
  <button type="button" data-id="1"><span>記事を開く</span></button>
</div>
const articleList = document.querySelector('#article-list');

articleList.addEventListener('click', (event) => {
  console.log(event.target);
  console.log(event.currentTarget);
});

targetは実際にクリックされた要素、currentTargetはイベントを登録した要素です。spanをクリックすると両者は異なります。

closestで操作対象を探す

articleList.addEventListener('click', (event) => {
  const button = event.target.closest('button[data-id]');

  if (!button || !articleList.contains(button)) {
    return;
  }

  console.log(button.dataset.id);
});

closest()は自分自身から親方向へ条件に合う要素を探します。対象外なら早期リターンします。

動的に追加した要素にも対応する

イベントは親へ登録済みなので、後からappend()したボタンのクリックも受け取れます。これをイベント委譲と呼びます。

stopPropagation()で伝播を止める方法もありますが、多用すると外側の処理が動かない原因になります。必要な理由が明確な場合だけ使います。

複数種類の操作を親要素で受け取る

操作の種類をdata属性へ記述する

<ul id="article-list">
  <li data-id="38">
    <span>JavaScriptの歴史</span>
    <button type="button" data-action="open"><span>詳細</span></button>
    <button type="button" data-action="remove"><span>削除</span></button>
  </li>
  <li data-id="39">
    <span>JavaScriptを動かす</span>
    <button type="button" data-action="open"><span>詳細</span></button>
    <button type="button" data-action="remove"><span>削除</span></button>
  </li>
</ul>

<p id="action-message" aria-live="polite"></p>

ボタン内のspanをクリックしても操作できるよう、event.targetそのものではなくclosest()でボタンを探します。

actionごとに処理を分ける

const articleList = document.querySelector('#article-list');
const actionMessage = document.querySelector('#action-message');

articleList.addEventListener('click', (event) => {
  const button = event.target.closest('button[data-action]');

  if (!button || !articleList.contains(button)) {
    return;
  }

  const item = button.closest('li[data-id]');
  const articleId = Number(item.dataset.id);
  const action = button.dataset.action;

  if (action === 'open') {
    actionMessage.textContent = `記事ID ${articleId}の詳細を開きます。`;
    return;
  }

  if (action === 'remove') {
    item.remove();
    actionMessage.textContent = `記事ID ${articleId}を削除しました。`;
  }
});

イベントリスナーは一覧全体に一つだけです。ボタンが100個あっても、各ボタンへ個別登録する必要がありません。

後から追加した要素で確認する

const newItem = document.createElement('li');
newItem.dataset.id = '40';
newItem.innerHTML = `
  <span>変数とデータ型</span>
  <button type="button" data-action="open">詳細</button>
  <button type="button" data-action="remove">削除</button>
`;
articleList.append(newItem);

この例のHTML文字列は開発者が固定で記述しています。利用者の入力をinnerHTMLへ埋め込んではいけません。

追加後のボタンにも、親に登録済みのイベントが伝わります。これが、動的な一覧でイベント委譲が役立つ理由です。

伝播を調査する方法

Consoleへevent.targetevent.currentTargetbuttonを順番に表示します。どの要素をクリックしたか、どこでイベントを受けたか、最終的にどの操作要素を採用したかを区別できます。

イベントが二重に動く場合は、子と親の両方へ同じ目的のリスナーを登録していないか確認します。すぐにstopPropagation()を追加するのではなく、登録場所を整理してください。

まとめ

イベント委譲では、親でイベントを受け取り、targetclosest()から操作対象を特定します。次の記事ではブラウザへ設定値を保存します。