【完全保存版】JavaScriptイベントリスナー一覧|addEventListenerの使い方・主要イベント

js-event-listener javascript
記事内に広告が含まれています。

JavaScriptでWebサイトを作っていると、「どのイベントを使えばいいのか分からない」「似たようなイベントの違いが曖昧で迷う」といった悩みに直面する方は多いのではないでしょうか。例えば、フォーム入力時にinputchangeのどちらを使うべきか、ボタン操作ではclickだけで十分なのか、それともmousedownmouseupを組み合わせるべきか…。さらに、コードを書いているうちに「このイベントリスナー、ちゃんと削除できているのかな?」「複数回発火してしまうのはなぜ?」といった不安も生まれます。公式ドキュメントを調べても情報が断片的で、実務で使いやすい形に整理されている記事は意外と少ないものです。

そこで本記事では、JavaScriptで扱えるイベントリスナーを体系的に整理し、初心者から中級者までが実装に役立てられるように分かりやすく解説します。基本的な使い方から、addEventListenerの構文や第3引数オプションの活用法、主要なイベント一覧、削除やデバッグの方法までまとめてチェックできる内容になっています。「イベントまわりの基礎を一気に押さえたい」「明日からの開発にすぐ役立てたい」という方に最適なリファレンス記事です。

この記事を読んでわかること

  • イベントリスナーの基礎知識と、addEventListeneronclickの違い
  • マウス・キーボード・フォーム・タッチ・ウィンドウ関連イベントの一覧と使い分け
  • 第3引数オプション(oncepassivecaptureなど)の意味と活用法
  • removeEventListenerによる削除や、getEventListenersを使った確認方法
  • バブリング・キャプチャリングの仕組みとイベントデリゲーションの実践方法
  • 実務でよくあるエラーやトラブルを回避するためのヒント

この記事を読み終えるころには、イベントリスナーを「なんとなく」ではなく「意図して正しく選べる」ようになり、効率的に実装できる自信がつくはずです。あなたの開発スピードとコードの品質を一段上げるために、ぜひ最後までご覧ください。

JavaScriptイベントリスナーの基本と使い方

イベントリスナーとは?初心者向け基礎知識

JavaScriptのイベントリスナーとは、ユーザーの操作(イベント)を検知して、特定の処理を実行するための仕組みです。

想像してみてください。あなたがWebサイトのボタンをクリックしたとき、画像が切り替わったり、フォームが送信されたりしますよね。これらはすべて、イベントリスナーが「ボタンがクリックされた」というイベントを検知し、対応する処理を実行しているからなのです。

まるで、あなたの家のドアベルのようなものです。ドアベル(イベント)が鳴ると、あなた(イベントリスナー)がその音を聞いて、ドアを開ける(処理を実行)という流れですね。

イベント・イベントハンドラ・イベントリスナーの違い

Web開発でよく混同される3つの用語を整理しましょう:

  • イベント : ユーザーの操作や、ブラウザで発生する出来事そのもの(クリック、キー入力、ページ読み込みなど)
  • イベントハンドラ : イベントが発生したときに実行される関数のこと
  • イベントリスナー : 特定のイベントを「待ち受けて」、そのイベントが発生したらイベントハンドラを実行する仕組み全体

つまり、イベントリスナーがイベントを検知し、イベントハンドラを呼び出すという関係になっています。

<!DOCTYPE html>
<html>
<head>
    <title>イベントリスナーの基本例</title>
</head>
<body>
    <button id="myButton">クリックしてね!</button>
    <p id="message">まだクリックされていません</p>

    <script>
        // イベントハンドラ(実行する関数)
        function handleClick() {
            document.getElementById('message').textContent = 'ボタンがクリックされました!';
        }

        // イベントリスナーの登録
        const button = document.getElementById('myButton');
        button.addEventListener('click', handleClick);
        // ↑ 'click'がイベント、handleClickがイベントハンドラ
    </script>
</body>
</html>

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

addEventListenerとonclick、onloadの違いと使い分け

JavaScriptでイベントを扱う方法は大きく分けて2つあります。それぞれの特徴を理解して、適切に使い分けましょう。

1. addEventListener(モダンな書き方・推奨)

addEventListenerは現在推奨されている方法で、以下のような特徴があります:

メリット:

  • 同じ要素・同じイベントに対して複数のイベントリスナーを登録できる
  • イベントの詳細な制御(バブリング、キャプチャリング)が可能
  • removeEventListenerで簡単に削除できる
  • より柔軟で保守性が高い
const button = document.getElementById('myButton');

// 複数のイベントリスナーを登録できる
button.addEventListener('click', function() {
    console.log('1番目の処理');
});

button.addEventListener('click', function() {
    console.log('2番目の処理');
});

// 両方とも実行される!

EventTarget: addEventListener() メソッド - Web API | MDN
addEventListener() は EventTarget インターフェイスのメソッドで、ターゲットに特定のイベントが配信されるたびに呼び出される関数を設定します。

2. onclickなどのインラインイベントハンドラ(従来の書き方)

onclickonloadonmouseoverなどは従来からある書き方です:

デメリット:

  • 同じイベントに対して1つのハンドラしか登録できない
  • 後から登録したハンドラが前のものを上書きしてしまう
  • イベントの詳細制御ができない
const button = document.getElementById('myButton');

// 古い書き方
button.onclick = function() {
    console.log('1番目の処理');
};

button.onclick = function() {
    console.log('2番目の処理');  // これだけが実行される(上書き)
};

// HTMLでの書き方も可能だが推奨されない
// <button onclick="handleClick()">クリック</button>

なぜaddEventListenerが推奨されるのか?

  1. 柔軟性: 複数のライブラリやモジュールが同じ要素にイベントリスナーを追加しても競合しない
  2. 保守性: コードが分かりやすく、後からの変更や削除が容易
  3. 標準準拠: Web標準に準拠した現代的な書き方
  4. 機能性: より多くのオプションと制御が可能

addEventListenerの構文と第3引数オプション(once, passiveなど)

addEventListenerの基本構文は以下の通りです:

element.addEventListener('イベント名', 処理する関数, オプション);

基本的な使い方

// 基本形
document.getElementById('myButton').addEventListener('click', function() {
    alert('ボタンがクリックされました!');
});

// アロー関数での書き方
document.getElementById('myButton').addEventListener('click', () => {
    alert('ボタンがクリックされました!');
});

// 別で定義した関数を指定
function handleClick() {
    alert('ボタンがクリックされました!');
}
document.getElementById('myButton').addEventListener('click', handleClick);

第3引数のオプション詳細解説

第3引数には、イベントリスナーの動作を細かく制御するオプションを指定できます。

1. onceオプション(一度だけ実行)

once: trueを指定すると、イベントリスナーは一度だけ実行された後、自動的に削除されます。

<!DOCTYPE html>
<html>
<body>
    <button id="onceButton">一度だけクリック可能</button>
    <p id="clickCount">クリック回数: 0</p>

    <script>
        let count = 0;

        document.getElementById('onceButton').addEventListener('click', function() {
            count++;
            document.getElementById('clickCount').textContent = `クリック回数: ${count}`;
            alert('このメッセージは一度だけ表示されます');
        }, { once: true });

        // 何度ボタンを押しても、アラートは最初の1回だけ!
    </script>
</body>
</html>

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

2. passiveオプション(スクロールパフォーマンス改善)

passive: trueを指定すると、イベントハンドラ内でpreventDefault()を呼び出さないことをブラウザに約束します。これにより、スクロールパフォーマンスが大幅に改善されます。

// スクロールイベントでpassiveを使用(パフォーマンス向上)
window.addEventListener('scroll', function() {
    console.log('スクロール中...');
    // この中でpreventDefault()は使えない(使っても無効)
}, { passive: true });

// タッチイベントでもpassiveが効果的
document.addEventListener('touchmove', function(e) {
    console.log('タッチ移動中...');
    // e.preventDefault()は無効
}, { passive: true });

3. その他の有用なオプション

// 複数のオプションを組み合わせ
document.getElementById('myElement').addEventListener('click', handleClick, {
    once: true,        // 一度だけ実行
    passive: false,    // preventDefault()を使用する可能性がある
    capture: true      // キャプチャフェーズで実行
});

// 古いブラウザ対応(boolean値で指定)
document.getElementById('myElement').addEventListener('click', handleClick, true);
// ↑ これはcapture: trueと同じ意味

実践的な使用例

<!DOCTYPE html>
<html>
<body>
    <div id="container" style="height: 200px; overflow-y: scroll; border: 1px solid #ccc;">
        <div style="height: 1000px; background: linear-gradient(to bottom, #f0f0f0, #333);">
            スクロール可能なコンテンツ
        </div>
    </div>

    <button id="subscribeButton">購読する(一回限り)</button>
    <p id="status"></p>

    <script>
        // パフォーマンス重視のスクロールイベント
        document.getElementById('container').addEventListener('scroll', function(e) {
            const status = document.getElementById('status');
            status.textContent = `スクロール位置: ${Math.round(e.target.scrollTop)}px`;
        }, { passive: true });

        // 一度だけ実行される購読ボタン
        document.getElementById('subscribeButton').addEventListener('click', function() {
            alert('購読ありがとうございます!');
            this.textContent = '購読済み';
            this.disabled = true;
        }, { once: true });
    </script>
</body>
</html>

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

この基本的な知識を身につけることで、より効率的で保守性の高いJavaScriptコードが書けるようになります。次のセクションでは、具体的にどのようなイベントが使えるのかを詳しく見ていきましょう。

JavaScriptで使えるイベントリスナー一覧と主要イベント解説

JavaScriptには数多くのイベントタイプが用意されており、それぞれ異なる場面で活用されます。ここでは実務でよく使われる主要なイベントを分類別に詳しく解説していきます。

マウスイベント(click、mouseover、mouseleaveなど)

マウス操作に関するイベントは、Webサイトのインタラクティブな機能を作る上で最も基本的で重要なイベント群です。

主要なマウスイベント一覧

イベント名発生タイミング用途例
clickマウスボタンをクリック(押して離す)ボタン操作、リンク遷移
dblclickマウスボタンをダブルクリックファイル開く、詳細表示
mousedownマウスボタンを押した瞬間ドラッグ開始の検知
mouseupマウスボタンを離した瞬間ドラッグ終了の検知
mousemoveマウスカーソルが移動マウス座標の追跡
mouseenter要素にマウスが入った時(子要素では発火しない)ホバーエフェクト
mouseleave要素からマウスが出た時(子要素では発火しない)ホバーエフェクト終了
mouseover要素にマウスが入った時(子要素でも発火)詳細なホバー制御
mouseout要素からマウスが出た時(子要素でも発火)詳細なホバー制御

実践的なコード例

<div class="demo-box" id="mouseBox">
  <div class="child-element">子要素</div>
</div>

<button id="clearLog">ログをクリア</button>
<div id="log"></div>
.demo-box {
  width: 200px;
  height: 200px;
  border: 2px solid #333;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 20px;
  cursor: pointer;
  transition: all 0.3s ease;
}
.demo-box:hover {
  background-color: #f0f0f0;
}
.child-element {
  width: 100px;
  height: 100px;
  background-color: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
}
#log {
  border: 1px solid #ccc;
  height: 200px;
  overflow-y: auto;
  padding: 10px;
  font-family: monospace;
}
const box = document.getElementById("mouseBox");
const log = document.getElementById("log");

function addLog(message) {
  const time = new Date().toLocaleTimeString();
  log.innerHTML += `[${time}] ${message}<br>`;
  log.scrollTop = log.scrollHeight;
}

// 各種マウスイベントの登録
box.addEventListener("click", () => addLog("✨ click - クリックされました"));
box.addEventListener("dblclick", () =>
  addLog("🔥 dblclick - ダブルクリックされました")
);
box.addEventListener("mousedown", () =>
  addLog("⬇️ mousedown - マウスボタンが押されました")
);
box.addEventListener("mouseup", () =>
  addLog("⬆️ mouseup - マウスボタンが離されました")
);

// mouseenterとmouseleaveは子要素では発火しない
box.addEventListener("mouseenter", () =>
  addLog("👉 mouseenter - 要素に入りました(子要素無視)")
);
box.addEventListener("mouseleave", () =>
  addLog("👈 mouseleave - 要素から出ました(子要素無視)")
);

// mouseoverとmouseoutは子要素でも発火する
box.addEventListener("mouseover", (e) =>
  addLog(`🔄 mouseover - ${e.target.textContent || "メイン要素"}にホバー`)
);
box.addEventListener("mouseout", (e) =>
  addLog(`🔄 mouseout - ${e.target.textContent || "メイン要素"}からホバー解除`)
);

// マウス座標の追跡
box.addEventListener("mousemove", (e) => {
  const rect = box.getBoundingClientRect();
  const x = Math.round(e.clientX - rect.left);
  const y = Math.round(e.clientY - rect.top);
  box.title = `座標: (${x}, ${y})`;
});

// ログクリア機能
document.getElementById("clearLog").addEventListener("click", () => {
  log.innerHTML = "";
});

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

キーボードイベント(keydown、keyup、keypress)

キーボード入力を扱うイベントは、フォーム操作やショートカットキー、ゲーム開発などで重要な役割を果たします。

3つのキーボードイベントの違い

イベント名発生タイミング特徴推奨用途
keydownキーが押された瞬間キーを押し続けると連続発火<br>すべてのキー(Shift、Ctrl等)で発火ショートカットキー<br>ゲームの操作
keyupキーが離された瞬間1回のキー操作で1回だけ発火<br>すべてのキー(Shift、Ctrl等)で発火キー入力の終了検知
keypress文字キーが押された時非推奨(削除予定)<br>文字キーのみで発火使用しない

実務的なアドバイス

  • keydown: リアルタイムな入力検知が必要な場合(ゲーム、ショートカットなど)
  • keyup: 1回の入力として扱いたい場合(入力完了の検知など)
  • keypress: 現在は非推奨のため使用を避ける

実践的なコード例

<div class="keyboard-demo">
  <h3>キーボードイベント体験デモ</h3>
  <input type="text" id="textInput" placeholder="ここに入力してキーイベントを確認">

  <div class="shortcut-info">
    <strong>試してみよう:</strong>
    <ul>
      <li>Ctrl + S : 保存のショートカット</li>
      <li>Ctrl + C : コピーのショートカット</li>
      <li>Escape : クリア機能</li>
      <li>Enter : 確定機能</li>
    </ul>
  </div>

  <div class="key-display" id="keyDisplay">キーを押してください</div>
  <div id="keyLog"></div>
</div>
.keyboard-demo {
  padding: 20px;
  border: 1px solid #ddd;
  margin: 10px 0;
}
.key-display {
  font-size: 24px;
  font-weight: bold;
  padding: 10px;
  background-color: #f8f9fa;
  border: 1px solid #dee2e6;
  border-radius: 4px;
  margin: 5px 0;
}
.pressed {
  background-color: #007bff !important;
  color: white;
}
#textInput {
  width: 100%;
  padding: 10px;
  font-size: 16px;
  border: 2px solid #ddd;
  border-radius: 4px;
}
.shortcut-info {
  background-color: #e7f3ff;
  padding: 10px;
  border-radius: 4px;
  margin: 10px 0;
}
const textInput = document.getElementById("textInput");
const keyDisplay = document.getElementById("keyDisplay");
const keyLog = document.getElementById("keyLog");

let pressedKeys = new Set(); // 現在押されているキーを管理

function addToLog(message) {
  const time = new Date().toLocaleTimeString();
  keyLog.innerHTML = `[${time}] ${message}<br>` + keyLog.innerHTML;

  // ログが多くなりすぎないよう制限
  const logs = keyLog.querySelectorAll("br");
  if (logs.length > 20) {
    keyLog.innerHTML = keyLog.innerHTML.split("<br>").slice(0, 10).join("<br>");
  }
}

// keydown: キーが押された瞬間(連続発火する)
textInput.addEventListener("keydown", function (e) {
  const key = e.key;
  const code = e.code;

  // 押されているキーを記録
  pressedKeys.add(key);

  // 表示を更新
  keyDisplay.textContent = `押下中: ${Array.from(pressedKeys).join(" + ")}`;
  keyDisplay.classList.add("pressed");

  // ショートカットキーの処理
  if (e.ctrlKey && e.key === "s") {
    e.preventDefault(); // ブラウザのデフォルト保存を阻止
    addToLog("🔥 Ctrl+S: 保存ショートカットが実行されました");
    alert("保存しました!(デモ)");
  } else if (e.ctrlKey && e.key === "c") {
    addToLog("📋 Ctrl+C: コピーショートカット検知");
  } else if (e.key === "Escape") {
    textInput.value = "";
    keyLog.innerHTML = "";
    addToLog("🧹 Escape: 入力とログがクリアされました");
  } else if (e.key === "Enter") {
    addToLog(`✅ Enter: "${textInput.value}" が確定されました`);
  }

  addToLog(`⬇️ keydown: ${key} (code: ${code})`);
});

// keyup: キーが離された瞬間(1回だけ発火)
textInput.addEventListener("keyup", function (e) {
  const key = e.key;

  // 離されたキーを記録から削除
  pressedKeys.delete(key);

  // 表示を更新
  if (pressedKeys.size === 0) {
    keyDisplay.textContent = "キーを押してください";
    keyDisplay.classList.remove("pressed");
  } else {
    keyDisplay.textContent = `押下中: ${Array.from(pressedKeys).join(" + ")}`;
  }

  addToLog(`⬆️ keyup: ${key}`);
});

// input: 実際の入力値の変更(日本語入力にも対応)
textInput.addEventListener("input", function (e) {
  addToLog(`📝 input: 入力値が変更 "${e.target.value}"`);
});

// フォーカス管理
textInput.addEventListener("focus", () => {
  addToLog("🎯 focus: 入力フィールドにフォーカス");
});

textInput.addEventListener("blur", () => {
  addToLog("😴 blur: 入力フィールドからフォーカス離脱");
  pressedKeys.clear();
  keyDisplay.textContent = "キーを押してください";
  keyDisplay.classList.remove("pressed");
});

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

フォームイベント(input、change、submitなど)

フォーム操作は Web アプリケーションの中核機能です。各イベントの特徴を理解して適切に使い分けることが重要です。

主要なフォームイベント

イベント名発生タイミング特徴使用場面
input入力値がリアルタイムで変更される度日本語入力(IME)にも対応リアルタイム検索、文字数カウント
change値が変更されてフォーカスが外れた時確定した変更のみ検知バリデーション、設定保存
focus要素にフォーカスが当たった時入力開始の検知プレースホルダー変更、ヘルプ表示
blur要素からフォーカスが外れた時入力終了の検知バリデーション実行
submitフォームが送信される時フォーム全体の送信検知送信前バリデーション

inputchangeの違いを詳しく解説

この2つのイベントの違いは初心者がつまずきやすいポイントです:

<!DOCTYPE html>
<html>
<body>
    <div style="padding: 20px;">
        <h3>inputとchangeの違いを体験</h3>

        <div style="margin: 20px 0;">
            <label for="textDemo">テキスト入力:</label><br>
            <input type="text" id="textDemo" placeholder="文字を入力してください">
            <p id="textStatus">何も入力されていません</p>
        </div>

        <div style="margin: 20px 0;">
            <label for="selectDemo">セレクトボックス:</label><br>
            <select id="selectDemo">
                <option value="">選択してください</option>
                <option value="apple">りんご</option>
                <option value="banana">バナナ</option>
                <option value="orange">オレンジ</option>
            </select>
            <p id="selectStatus">何も選択されていません</p>
        </div>

        <div style="margin: 20px 0;">
            <label>
                <input type="checkbox" id="checkDemo">
                チェックボックス
            </label>
            <p id="checkStatus">チェックされていません</p>
        </div>

        <div id="eventLog" style="border: 1px solid #ccc; height: 200px; overflow-y: auto; padding: 10px; font-family: monospace;"></div>
    </div>

    <script>
        const eventLog = document.getElementById('eventLog');

        function logEvent(type, element, value) {
            const time = new Date().toLocaleTimeString();
            eventLog.innerHTML = `[${time}] ${type}: ${element} = "${value}"<br>` + eventLog.innerHTML;
        }

        // テキスト入力での input vs change
        const textDemo = document.getElementById('textDemo');
        const textStatus = document.getElementById('textStatus');

        textDemo.addEventListener('input', function(e) {
            textStatus.textContent = `入力中: "${e.target.value}"`;
            textStatus.style.color = '#007bff';
            logEvent('INPUT', 'テキスト', e.target.value);
        });

        textDemo.addEventListener('change', function(e) {
            textStatus.textContent = `確定: "${e.target.value}"`;
            textStatus.style.color = '#28a745';
            logEvent('CHANGE', 'テキスト', e.target.value);
        });

        // セレクトボックスでの change
        const selectDemo = document.getElementById('selectDemo');
        const selectStatus = document.getElementById('selectStatus');

        selectDemo.addEventListener('change', function(e) {
            const selectedText = e.target.options[e.target.selectedIndex].text;
            selectStatus.textContent = `選択: ${selectedText}`;
            logEvent('CHANGE', 'セレクト', selectedText);
        });

        // チェックボックスでの change
        const checkDemo = document.getElementById('checkDemo');
        const checkStatus = document.getElementById('checkStatus');

        checkDemo.addEventListener('change', function(e) {
            const status = e.target.checked ? 'チェック済み' : 'チェック解除';
            checkStatus.textContent = status;
            logEvent('CHANGE', 'チェックボックス', status);
        });
    </script>
</body>
</html>

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

フォーム送信とバリデーション

<!DOCTYPE html>
<html>
<body>
    <form id="demoForm" style="max-width: 400px; margin: 20px; padding: 20px; border: 1px solid #ddd;">
        <h3>フォーム送信イベントのデモ</h3>

        <div style="margin: 10px 0;">
            <label for="username">ユーザー名 (必須):</label><br>
            <input type="text" id="username" name="username" required>
            <span id="usernameError" style="color: red; font-size: 12px;"></span>
        </div>

        <div style="margin: 10px 0;">
            <label for="email">メールアドレス (必須):</label><br>
            <input type="email" id="email" name="email" required>
            <span id="emailError" style="color: red; font-size: 12px;"></span>
        </div>

        <button type="submit">送信</button>
        <button type="reset">リセット</button>
    </form>

    <div id="formLog" style="margin: 20px; padding: 10px; border: 1px solid #ccc; font-family: monospace;"></div>

    <script>
        const form = document.getElementById('demoForm');
        const formLog = document.getElementById('formLog');

        function logFormEvent(message) {
            const time = new Date().toLocaleTimeString();
            formLog.innerHTML = `[${time}] ${message}<br>` + formLog.innerHTML;
        }

        // リアルタイムバリデーション(input イベント)
        document.getElementById('username').addEventListener('input', function(e) {
            const error = document.getElementById('usernameError');
            if (e.target.value.length < 3) {
                error.textContent = '3文字以上入力してください';
            } else {
                error.textContent = '';
            }
        });

        // 確定時バリデーション(blur イベント)
        document.getElementById('email').addEventListener('blur', function(e) {
            const error = document.getElementById('emailError');
            const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;

            if (e.target.value && !emailRegex.test(e.target.value)) {
                error.textContent = '正しいメールアドレス形式で入力してください';
            } else {
                error.textContent = '';
            }
        });

        // フォーカスイベント
        form.addEventListener('focus', function(e) {
            if (e.target.matches('input')) {
                logFormEvent(`フォーカス: ${e.target.name}フィールド`);
            }
        }, true); // キャプチャフェーズで実行

        // フォーム送信イベント
        form.addEventListener('submit', function(e) {
            e.preventDefault(); // 実際の送信を阻止(デモのため)

            logFormEvent('📤 フォーム送信が試行されました');

            // カスタムバリデーション
            const username = document.getElementById('username').value;
            const email = document.getElementById('email').value;

            if (username.length < 3) {
                logFormEvent('❌ 送信失敗: ユーザー名が短すぎます');
                alert('ユーザー名は3文字以上で入力してください');
                return;
            }

            logFormEvent('✅ 送信成功: すべてのバリデーションをクリア');
            alert(`送信成功!\\nユーザー名: ${username}\\nメール: ${email}`);
        });

        // リセットイベント
        form.addEventListener('reset', function(e) {
            logFormEvent('🔄 フォームがリセットされました');

            // エラーメッセージもクリア
            document.getElementById('usernameError').textContent = '';
            document.getElementById('emailError').textContent = '';
        });
    </script>
</body>
</html>

実際の表示

See the Pen js-event-listener-07 by watashi-xyz (@watashi-xyz) on CodePen.

タッチイベント・ドラッグ&ドロップイベント

モバイル対応とユーザビリティ向上に欠かせないイベント群です。

タッチイベント(モバイル対応に必須)

モバイルデバイスでの操作を検知するためのイベントです:

イベント名発生タイミング用途
touchstart画面にタッチした瞬間タッチ開始の検知
touchmoveタッチしたまま移動スワイプ、ドラッグ操作
touchend画面から指を離した瞬間タッチ終了の検知
touchcancelタッチが中断された時割り込み処理
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .touch-area {
            width: 300px;
            height: 200px;
            border: 2px solid #007bff;
            background-color: #f8f9fa;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 20px auto;
            touch-action: none; /* ブラウザのデフォルト動作を無効化 */
            user-select: none;
        }
        .touch-info {
            font-family: monospace;
            background-color: #e9ecef;
            padding: 10px;
            border-radius: 4px;
            margin: 10px;
        }
    </style>
</head>
<body>
    <div class="touch-area" id="touchArea">
        タッチしてください
    </div>

    <div class="touch-info" id="touchInfo">
        タッチ情報がここに表示されます
    </div>

    <script>
        const touchArea = document.getElementById('touchArea');
        const touchInfo = document.getElementById('touchInfo');

        let startX = 0, startY = 0;
        let currentX = 0, currentY = 0;

        touchArea.addEventListener('touchstart', function(e) {
            e.preventDefault(); // スクロール等を防止

            const touch = e.touches[0];
            startX = touch.clientX;
            startY = touch.clientY;
            currentX = startX;
            currentY = startY;

            touchArea.style.backgroundColor = '#007bff';
            touchArea.style.color = 'white';
            touchArea.textContent = 'タッチ中...';

            touchInfo.innerHTML = `
                🟢 touchstart<br>
                開始位置: (${Math.round(startX)}, ${Math.round(startY)})<br>
                タッチ点数: ${e.touches.length}
            `;
        }, { passive: false });

        touchArea.addEventListener('touchmove', function(e) {
            e.preventDefault();

            const touch = e.touches[0];
            currentX = touch.clientX;
            currentY = touch.clientY;

            const deltaX = currentX - startX;
            const deltaY = currentY - startY;
            const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);

            touchInfo.innerHTML = `
                🔄 touchmove<br>
                現在位置: (${Math.round(currentX)}, ${Math.round(currentY)})<br>
                移動距離: ${Math.round(distance)}px<br>
                方向: (${deltaX > 0 ? '右' : '左'}, ${deltaY > 0 ? '下' : '上'})
            `;
        }, { passive: false });

        touchArea.addEventListener('touchend', function(e) {
            const deltaX = currentX - startX;
            const deltaY = currentY - startY;
            const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);

            touchArea.style.backgroundColor = '#28a745';
            touchArea.textContent = 'タッチ完了!';

            let gesture = 'タップ';
            if (distance > 50) {
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    gesture = deltaX > 0 ? '右スワイプ' : '左スワイプ';
                } else {
                    gesture = deltaY > 0 ? '下スワイプ' : '上スワイプ';
                }
            }

            touchInfo.innerHTML = `
                ✅ touchend<br>
                ジェスチャー: ${gesture}<br>
                総移動距離: ${Math.round(distance)}px
            `;

            // 1秒後にリセット
            setTimeout(() => {
                touchArea.style.backgroundColor = '#f8f9fa';
                touchArea.style.color = 'black';
                touchArea.textContent = 'タッチしてください';
            }, 1000);
        });

        // マウス操作にも対応(開発時のテスト用)
        let isMouseDown = false;

        touchArea.addEventListener('mousedown', function(e) {
            isMouseDown = true;
            startX = e.clientX;
            startY = e.clientY;
            touchArea.style.backgroundColor = '#007bff';
            touchArea.style.color = 'white';
            touchArea.textContent = 'ドラッグ中...';
        });

        touchArea.addEventListener('mousemove', function(e) {
            if (isMouseDown) {
                currentX = e.clientX;
                currentY = e.clientY;
                const deltaX = currentX - startX;
                const deltaY = currentY - startY;

                touchInfo.innerHTML = `
                    🖱️ マウスドラッグ中<br>
                    移動量: (${deltaX}, ${deltaY})
                `;
            }
        });

        touchArea.addEventListener('mouseup', function() {
            isMouseDown = false;
            touchArea.style.backgroundColor = '#28a745';
            touchArea.textContent = 'ドラッグ完了!';
        });
    </script>
</body>
</html>

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

ドラッグ&ドロップイベント

HTML5 Drag and Drop API を使用したイベントです:

<!DOCTYPE html>
<html>
<head>
    <style>
        .drag-container {
            display: flex;
            gap: 20px;
            padding: 20px;
        }
        .drag-item {
            width: 100px;
            height: 100px;
            background-color: #007bff;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: move;
            border-radius: 8px;
        }
        .drop-zone {
            width: 200px;
            height: 200px;
            border: 3px dashed #ccc;
            display: flex;
            align-items: center;
            justify-content: center;
            background-color: #f8f9fa;
            border-radius: 8px;
        }
        .drop-zone.drag-over {
            border-color: #007bff;
            background-color: #e3f2fd;
        }
        .dragging {
            opacity: 0.5;
        }
    </style>
</head>
<body>
    <h3>ドラッグ&ドロップ デモ</h3>

    <div class="drag-container">
        <div class="drag-item" draggable="true" data-id="item1">
            アイテム1
        </div>
        <div class="drag-item" draggable="true" data-id="item2">
            アイテム2
        </div>
        <div class="drop-zone" id="dropZone">
            ここにドロップ
        </div>
    </div>

    <div id="dragLog" style="margin: 20px 0; padding: 10px; border: 1px solid #ccc; font-family: monospace;"></div>

    <script>
        const dragItems = document.querySelectorAll('.drag-item');
        const dropZone = document.getElementById('dropZone');
        const dragLog = document.getElementById('dragLog');

        function logDragEvent(message) {
            const time = new Date().toLocaleTimeString();
            dragLog.innerHTML = `[${time}] ${message}<br>` + dragLog.innerHTML;
        }

        // ドラッグ開始
        dragItems.forEach(item => {
            item.addEventListener('dragstart', function(e) {
                e.dataTransfer.setData('text/plain', e.target.dataset.id);
                e.dataTransfer.effectAllowed = 'move';
                e.target.classList.add('dragging');
                logDragEvent(`🚀 dragstart: ${e.target.textContent} のドラッグ開始`);
            });

            item.addEventListener('dragend', function(e) {
                e.target.classList.remove('dragging');
                logDragEvent(`🏁 dragend: ${e.target.textContent} のドラッグ終了`);
            });
        });

        // ドロップゾーンでのイベント
        dropZone.addEventListener('dragover', function(e) {
            e.preventDefault(); // ドロップを有効にする
            e.dataTransfer.dropEffect = 'move';
            this.classList.add('drag-over');
        });

        dropZone.addEventListener('dragenter', function(e) {
            e.preventDefault();
            logDragEvent('👋 dragenter: ドロップゾーンに入りました');
        });

        dropZone.addEventListener('dragleave', function(e) {
            this.classList.remove('drag-over');
            logDragEvent('👋 dragleave: ドロップゾーンから出ました');
        });

        dropZone.addEventListener('drop', function(e) {
            e.preventDefault();
            const itemId = e.dataTransfer.getData('text/plain');
            const draggedItem = document.querySelector(`[data-id="${itemId}"]`);

            this.classList.remove('drag-over');
            this.appendChild(draggedItem);

            logDragEvent(`✅ drop: ${draggedItem.textContent} がドロップされました`);
        });
    </script>
</body>
</html>

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

ウィンドウ・DOMイベント(DOMContentLoaded、scroll、resize)

ページ全体やブラウザウィンドウに関連するイベントは、Web アプリケーションの基盤となる重要なイベント群です。

主要なウィンドウ・DOMイベント

イベント名発生タイミング用途
DOMContentLoadedHTML の解析完了(画像等の読み込み前)スクリプト実行開始に最適
loadすべてのリソース読み込み完了画像サイズ取得等
scrollページまたは要素のスクロールスクロール連動アニメーション
resizeウィンドウサイズ変更レスポンシブ対応
beforeunloadページを離れる直前保存確認ダイアログ
visibilitychangeタブの表示/非表示切り替えパフォーマンス最適化

DOMContentLoaded vs load の重要な違い

この違いを理解することは非常に重要です:

<!DOCTYPE html>
<html>
<head>
    <script>
        // ページ読み込みタイミングの比較デモ
        const startTime = performance.now();

        function logTiming(eventName) {
            const elapsed = Math.round(performance.now() - startTime);
            console.log(`${eventName}: ${elapsed}ms`);

            const logElement = document.getElementById('timingLog');
            if (logElement) {
                logElement.innerHTML += `<div>${eventName}: ${elapsed}ms</div>`;
            }
        }

        // 1. DOMContentLoaded: HTML解析完了(推奨)
        document.addEventListener('DOMContentLoaded', function() {
            logTiming('📄 DOMContentLoaded');

            // この時点でDOM操作は安全に実行可能
            const button = document.getElementById('testButton');
            if (button) {
                button.addEventListener('click', function() {
                    alert('DOMContentLoadedで登録されたイベント!');
                });
            }
        });

        // 2. load: すべてのリソース読み込み完了
        window.addEventListener('load', function() {
            logTiming('🖼️ load (すべてのリソース完了)');

            // 画像のサイズが確定している
            const img = document.getElementById('testImage');
            if (img) {
                console.log(`画像サイズ: ${img.width} x ${img.height}`);
            }
        });
    </script>
</head>
<body>
    <h3>ページ読み込みイベントのタイミング比較</h3>
    <div id="timingLog" style="background-color: #f8f9fa; padding: 10px; border: 1px solid #ddd; font-family: monospace;"></div>

    <button id="testButton">テストボタン</button>

    <!-- 大きな画像を読み込んで、loadイベントとの差を確認 -->
    <img id="testImage" src="<https://picsum.photos/800/600>" alt="テスト画像" style="max-width: 100%; height: auto;">

    <div style="margin: 20px 0; padding: 10px; background-color: #e7f3ff; border-radius: 4px;">
        <strong>💡 重要なポイント:</strong>
        <ul>
            <li><code>DOMContentLoaded</code>: HTML解析完了時点で発火(速い)</li>
            <li><code>load</code>: 画像・CSS等すべてのリソース読み込み完了時点で発火(遅い)</li>
            <li>通常は<code>DOMContentLoaded</code>を使用することを推奨</li>
        </ul>
    </div>
</body>
</html>

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

スクロール・リサイズイベントの活用

<div class="scroll-indicator" id="scrollIndicator"></div>

<header class="header" id="header">
  <h1>スクロール・リサイズイベント デモ</h1>
</header>

<div class="info-panel" id="infoPanel">
  <div><strong>📊 リアルタイム情報</strong></div>
  <div>ウィンドウサイズ: <span id="windowSize">-</span></div>
  <div>スクロール位置: <span id="scrollPos">-</span></div>
  <div>スクロール率: <span id="scrollPercent">-</span></div>
  <div>ビューポート: <span id="viewport">-</span></div>
  <div>デバイス: <span id="deviceInfo">-</span></div>
</div>

<div class="content">
  <div class="section">
    <h2>セクション 1</h2>
    <p>このページをスクロールして、各種イベントの動作を確認してください。ウィンドウのサイズを変更することも試してみてください。</p>
  </div>

  <div class="section">
    <h2>セクション 2</h2>
    <p>スクロール位置に応じて、ヘッダーの透明度やスクロールインジケーターが変化します。</p>
  </div>

  <div class="section">
    <h2>セクション 3</h2>
    <p>リサイズイベントでレスポンシブな動作を実装できます。</p>
  </div>

  <div class="section">
    <h2>セクション 4</h2>
    <p>パフォーマンス向上のため、スクロールイベントではpassive: trueオプションを使用しています。</p>
  </div>
</div>
body {
  margin: 0;
  font-family: Arial, sans-serif;
}
.header {
  position: fixed;
  top: 0;
  width: 100%;
  background-color: #007bff;
  color: white;
  padding: 10px 20px;
  transition: all 0.3s ease;
  z-index: 1000;
}
.header.scrolled {
  background-color: rgba(0, 123, 255, 0.9);
  backdrop-filter: blur(10px);
}
.content {
  margin-top: 60px;
  padding: 20px;
  height: 200vh; /* スクロール可能にする */
  background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
}
.scroll-indicator {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background-color: #28a745;
  transition: width 0.1s ease;
  z-index: 1001;
}
.info-panel {
  position: fixed;
  top: 80px;
  right: 20px;
  background-color: white;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  font-family: monospace;
  font-size: 12px;
  max-width: 250px;
}
.section {
  margin: 50px 0;
  padding: 30px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
// 要素の取得
const header = document.getElementById("header");
const scrollIndicator = document.getElementById("scrollIndicator");
const infoPanel = document.getElementById("infoPanel");

// 情報表示用の要素
const windowSizeSpan = document.getElementById("windowSize");
const scrollPosSpan = document.getElementById("scrollPos");
const scrollPercentSpan = document.getElementById("scrollPercent");
const viewportSpan = document.getElementById("viewport");
const deviceInfoSpan = document.getElementById("deviceInfo");

// パフォーマンス向上のためのスロットル関数
function throttle(func, delay) {
  let timeoutId;
  let lastExecTime = 0;
  return function (...args) {
    const currentTime = Date.now();

    if (currentTime - lastExecTime > delay) {
      func.apply(this, args);
      lastExecTime = currentTime;
    } else {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        func.apply(this, args);
        lastExecTime = Date.now();
      }, delay - (currentTime - lastExecTime));
    }
  };
}

// 情報更新関数
function updateInfo() {
  // ウィンドウサイズ
  windowSizeSpan.textContent = `${window.innerWidth} x ${window.innerHeight}`;

  // スクロール位置
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  scrollPosSpan.textContent = `${Math.round(scrollTop)}px`;

  // スクロール率
  const documentHeight =
    document.documentElement.scrollHeight - window.innerHeight;
  const scrollPercent = (scrollTop / documentHeight) * 100;
  scrollPercentSpan.textContent = `${Math.round(scrollPercent)}%`;

  // スクロールインジケーターの更新
  scrollIndicator.style.width = `${scrollPercent}%`;

  // ヘッダーのスタイル変更
  if (scrollTop > 50) {
    header.classList.add("scrolled");
  } else {
    header.classList.remove("scrolled");
  }

  // ビューポート情報
  viewportSpan.textContent = `${window.innerWidth} x ${window.innerHeight}`;
}

// デバイス情報の初期設定
function updateDeviceInfo() {
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
    navigator.userAgent
  );
  const isTablet = /iPad|Android(?=.*Mobile)/i.test(navigator.userAgent);
  let deviceType = "Desktop";

  if (isMobile && !isTablet) deviceType = "Mobile";
  else if (isTablet) deviceType = "Tablet";

  deviceInfoSpan.textContent = `${deviceType} (${navigator.platform})`;
}

// スクロールイベント(パフォーマンス重視)
const throttledUpdateInfo = throttle(updateInfo, 16); // 60fps
window.addEventListener("scroll", throttledUpdateInfo, { passive: true });

// リサイズイベント
const throttledResize = throttle(function () {
  updateInfo();
  console.log(
    `📐 ウィンドウサイズ変更: ${window.innerWidth} x ${window.innerHeight}`
  );

  // レスポンシブ対応の例
  if (window.innerWidth < 768) {
    infoPanel.style.position = "static";
    infoPanel.style.margin = "10px";
  } else {
    infoPanel.style.position = "fixed";
    infoPanel.style.margin = "0";
  }
}, 100);

window.addEventListener("resize", throttledResize);

// ページの可視性変更(タブ切り替え)
document.addEventListener("visibilitychange", function () {
  if (document.hidden) {
    console.log("📱 ページが非表示になりました");
    // アニメーションやタイマーを停止するなど
  } else {
    console.log("📱 ページが表示されました");
    // アニメーションやタイマーを再開するなど
  }
});

// ページ離脱前の確認
window.addEventListener("beforeunload", function (e) {
  // 実際のアプリでは、未保存の変更がある場合のみ表示
  const hasUnsavedChanges = false; // ここで実際の状態をチェック

  if (hasUnsavedChanges) {
    const message = "変更が保存されていません。本当にページを離れますか?";
    e.returnValue = message;
    return message;
  }
});

// 初期化
updateInfo();
updateDeviceInfo();

// パフォーマンス測定
window.addEventListener("load", function () {
  const loadTime = performance.now();
  console.log(`⚡ ページ読み込み完了: ${Math.round(loadTime)}ms`);
});

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

イベントリスナーの管理・削除・デバッグ方法

効率的なWeb開発には、イベントリスナーの適切な管理が欠かせません。メモリリークを防ぎ、パフォーマンスを最適化し、デバッグを効率化するための実践的な手法を詳しく解説します。

removeEventListenerの正しい使い方と注意点

なぜイベントリスナーを削除する必要があるのか?

イベントリスナーを削除しないと、以下のような問題が発生します:

  1. メモリリーク: 不要なイベントリスナーがメモリに残り続ける
  2. パフォーマンス低下: 削除された要素に対してもイベントが発火し続ける
  3. 予期しない動作: 古いイベントリスナーが意図しない処理を実行する
  4. 重複実行: 同じ処理が複数回実行されてしまう
<div class="demo-container">
  <h3>🔧 removeEventListenerの正しい使い方</h3>

  <div class="button-group">
    <button class="add-btn" id="addListenerBtn">イベントリスナーを追加</button>
    <button class="remove-btn" id="removeListenerBtn">イベントリスナーを削除</button>
    <button class="test-btn" id="testBtn">テストボタン</button>
    <button class="clear-btn" id="clearLogBtn">ログクリア</button>
  </div>

  <div id="eventLog"></div>

  <div class="warning">
    <strong>⚠️ 重要な注意点:</strong> <code>removeEventListener</code>は、<code>addEventListener</code>と<strong>完全に同じ引数</strong>を指定する必要があります。
  </div>
</div>
.demo-container {
  padding: 20px;
  border: 1px solid #ddd;
  margin: 10px 0;
  background-color: #f8f9fa;
}
.button-group {
  margin: 10px 0;
}
.button-group button {
  margin: 5px;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.add-btn {
  background-color: #28a745;
  color: white;
}
.remove-btn {
  background-color: #dc3545;
  color: white;
}
.test-btn {
  background-color: #007bff;
  color: white;
}
.clear-btn {
  background-color: #6c757d;
  color: white;
}
#eventLog {
  background-color: white;
  border: 1px solid #ccc;
  height: 200px;
  overflow-y: auto;
  padding: 10px;
  font-family: monospace;
  font-size: 12px;
  margin: 10px 0;
}
.warning {
  background-color: #fff3cd;
  border: 1px solid #ffeaa7;
  padding: 10px;
  border-radius: 4px;
  margin: 10px 0;
}
.success {
  background-color: #d4edda;
  border: 1px solid #c3e6cb;
  padding: 10px;
  border-radius: 4px;
  margin: 10px 0;
}
const testBtn = document.getElementById("testBtn");
const eventLog = document.getElementById("eventLog");
const addListenerBtn = document.getElementById("addListenerBtn");
const removeListenerBtn = document.getElementById("removeListenerBtn");
const clearLogBtn = document.getElementById("clearLogBtn");

let listenerCount = 0;

function logEvent(message, type = "info") {
  const time = new Date().toLocaleTimeString();
  const color =
    type === "error" ? "red" : type === "success" ? "green" : "black";
  eventLog.innerHTML =
    `<div style="color: ${color}">[${time}] ${message}</div>` +
    eventLog.innerHTML;
}

// ✅ 正しい例: 名前付き関数を使用
function correctHandler() {
  logEvent("✅ 正しく削除可能なイベントハンドラが実行されました", "success");
}

// ❌ 間違った例: 無名関数(削除不可能)
let wrongHandler = function () {
  logEvent("❌ 削除できない無名関数ハンドラが実行されました", "error");
};

// イベントリスナー追加
addListenerBtn.addEventListener("click", function () {
  listenerCount++;

  // 正しい方法での追加
  testBtn.addEventListener("click", correctHandler);

  // 間違った方法での追加(無名関数)
  testBtn.addEventListener("click", function () {
    logEvent(
      `❌ 無名関数 ${listenerCount} が実行されました(削除不可)`,
      "error"
    );
  });

  logEvent(`📝 イベントリスナーを追加しました (追加回数: ${listenerCount})`);
});

// イベントリスナー削除
removeListenerBtn.addEventListener("click", function () {
  // ✅ 正しい削除方法
  testBtn.removeEventListener("click", correctHandler);
  logEvent("✅ 名前付き関数のイベントリスナーを削除しました", "success");

  // ❌ 間違った削除方法(効果なし)
  testBtn.removeEventListener("click", function () {
    // これは削除されない!同じ関数オブジェクトではないため
    logEvent("この処理は削除されません");
  });

  logEvent("⚠️ 無名関数のイベントリスナーは削除できませんでした", "error");
});

// ログクリア
clearLogBtn.addEventListener("click", function () {
  eventLog.innerHTML = "";
});

// 初期ログ
logEvent(
  "🚀 デモが開始されました。まず「イベントリスナーを追加」を押してください"
);

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

removeEventListenerの完全一致の原則

最も重要なポイント: removeEventListeneraddEventListener完全に同じ引数を指定する必要があります。

// ❌ 間違った例 - 削除されない
button.addEventListener('click', function() {
    console.log('クリックされました');
});

// これでは削除されない(別の関数オブジェクトのため)
button.removeEventListener('click', function() {
    console.log('クリックされました');
});

// ✅ 正しい例 - 正常に削除される
function handleClick() {
    console.log('クリックされました');
}

button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // 同じ関数参照

// ✅ オプション付きの場合も完全一致が必要
button.addEventListener('click', handleClick, { once: true });
button.removeEventListener('click', handleClick, { once: true }); // オプションも一致

// ❌ オプションが異なる場合は削除されない
button.removeEventListener('click', handleClick); // オプション未指定では削除されない

実践的なイベントリスナー管理パターン

<div id="dynamicContent">
  <button id="addItemBtn">アイテムを追加</button>
  <div id="itemContainer"></div>
</div>

<div style="margin: 20px 0; padding: 15px; background-color: #e7f3ff; border-radius: 4px;">
  <strong>💡 学習ポイント:</strong>
  <ul>
    <li>動的に作成される要素のイベントリスナー管理</li>
    <li>メモリリーク防止のための適切な削除タイミング</li>
    <li>クリーンアップ関数パターンの実装</li>
  </ul>
</div>
class ItemManager {
  constructor() {
    this.items = new Map(); // アイテムとクリーンアップ関数を管理
    this.itemCount = 0;
    this.init();
  }

  init() {
    const addBtn = document.getElementById("addItemBtn");
    addBtn.addEventListener("click", () => this.addItem());
  }

  addItem() {
    this.itemCount++;
    const container = document.getElementById("itemContainer");

    // 新しいアイテム要素を作成
    const itemDiv = document.createElement("div");
    itemDiv.innerHTML = `
                    <div style="border: 1px solid #ddd; margin: 5px 0; padding: 10px; border-radius: 4px;">
                        <span>アイテム ${this.itemCount}</span>
                        <button class="delete-btn" style="margin-left: 10px; background-color: #dc3545; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer;">削除</button>
                    </div>
                `;

    container.appendChild(itemDiv);

    // イベントハンドラを定義
    const deleteHandler = (e) => {
      console.log(`アイテム ${this.itemCount} が削除されます`);
      this.removeItem(itemDiv);
    };

    const mouseEnterHandler = (e) => {
      e.target.closest("div").style.backgroundColor = "#f8f9fa";
    };

    const mouseLeaveHandler = (e) => {
      e.target.closest("div").style.backgroundColor = "white";
    };

    // イベントリスナーを登録
    const deleteBtn = itemDiv.querySelector(".delete-btn");
    deleteBtn.addEventListener("click", deleteHandler);
    itemDiv.addEventListener("mouseenter", mouseEnterHandler);
    itemDiv.addEventListener("mouseleave", mouseLeaveHandler);

    // クリーンアップ関数を作成して保存
    const cleanup = () => {
      deleteBtn.removeEventListener("click", deleteHandler);
      itemDiv.removeEventListener("mouseenter", mouseEnterHandler);
      itemDiv.removeEventListener("mouseleave", mouseLeaveHandler);
      console.log(
        `アイテム ${this.itemCount} のイベントリスナーを全て削除しました`
      );
    };

    // アイテムとクリーンアップ関数を管理用Mapに保存
    this.items.set(itemDiv, cleanup);
  }

  removeItem(itemDiv) {
    // クリーンアップ関数を実行
    const cleanup = this.items.get(itemDiv);
    if (cleanup) {
      cleanup();
      this.items.delete(itemDiv);
    }

    // DOM から要素を削除
    itemDiv.remove();
  }

  // 全てのアイテムを削除(アプリケーション終了時など)
  destroy() {
    this.items.forEach((cleanup, itemDiv) => {
      cleanup();
      itemDiv.remove();
    });
    this.items.clear();
    console.log("ItemManager が正常にクリーンアップされました");
  }
}

// インスタンス作成
const itemManager = new ItemManager();

// ページ離脱時のクリーンアップ(推奨パターン)
window.addEventListener("beforeunload", () => {
  itemManager.destroy();
});

実際の表示

See the Pen js-event-listener-13 by watashi-xyz (@watashi-xyz) on CodePen.

getEventListenersで登録済みイベントを一覧取得する方法

getEventListenersは Chrome DevTools でのみ利用可能な非標準の関数ですが、デバッグ時に非常に有用なツールです。

基本的な使い方

<!DOCTYPE html>
<html>
<body>
    <div id="debugDemo" style="padding: 20px; border: 2px solid #007bff; margin: 20px; background-color: #f8f9fa;">
        <h3>🔍 getEventListeners デバッグデモ</h3>
        <button id="debugButton">デバッグ対象ボタン</button>
        <input type="text" id="debugInput" placeholder="入力してください">
        <p id="debugText">マウスを当ててください</p>
    </div>

    <div style="background-color: #fff3cd; padding: 15px; border-radius: 4px; margin: 20px;">
        <h4>🛠️ Chrome DevTools での使用手順:</h4>
        <ol>
            <li><kbd>F12</kbd>キーでDevToolsを開く</li>
            <li><strong>Console</strong>タブを選択</li>
            <li>以下のコマンドを試してください:</li>
        </ol>
        <div style="background-color: #f8f9fa; padding: 10px; border-radius: 4px; font-family: monospace; margin: 10px 0;">
            <div>// 特定の要素のイベントリスナーを確認</div>
            <div><strong>getEventListeners(document.getElementById('debugButton'))</strong></div>
            <br>
            <div>// ページ全体のイベントリスナーを確認</div>
            <div><strong>getEventListeners(document)</strong></div>
            <br>
            <div>// ウィンドウオブジェクトのイベントリスナーを確認</div>
            <div><strong>getEventListeners(window)</strong></div>
        </div>
    </div>

    <script>
        const debugButton = document.getElementById('debugButton');
        const debugInput = document.getElementById('debugInput');
        const debugText = document.getElementById('debugText');

        // 意図的に複数のイベントリスナーを登録(デバッグ用)

        // ボタンに複数のクリックイベント
        debugButton.addEventListener('click', function handler1() {
            console.log('👆 ハンドラー1: 通常のクリック処理');
        });

        debugButton.addEventListener('click', function handler2() {
            console.log('👆 ハンドラー2: ログ記録処理');
        }, { once: true });

        debugButton.addEventListener('click', function handler3() {
            console.log('👆 ハンドラー3: 分析処理');
        }, { passive: true });

        // 入力フィールドに複数のイベント
        debugInput.addEventListener('input', function(e) {
            console.log('📝 入力イベント:', e.target.value);
        });

        debugInput.addEventListener('focus', function() {
            console.log('🎯 フォーカスイベント');
        });

        debugInput.addEventListener('blur', function() {
            console.log('😴 ブラーイベント');
        });

        // テキスト要素にマウスイベント
        debugText.addEventListener('mouseenter', function() {
            this.style.backgroundColor = '#e3f2fd';
            console.log('🖱️ マウスエンター');
        });

        debugText.addEventListener('mouseleave', function() {
            this.style.backgroundColor = 'transparent';
            console.log('🖱️ マウスリーブ');
        });

        // ドキュメントレベルのイベント
        document.addEventListener('keydown', function(e) {
            if (e.ctrlKey && e.key === 'i') {
                console.log('🔍 Ctrl+I が押されました(検査ショートカット)');
            }
        });

        // ウィンドウレベルのイベント
        window.addEventListener('resize', function() {
            console.log('📐 ウィンドウリサイズ');
        });

        // デバッグ情報を自動的にコンソールに出力(Chrome限定)
        setTimeout(() => {
            if (typeof getEventListeners === 'function') {
                console.group('🔍 自動デバッグ情報');
                console.log('📝 debugButton のイベントリスナー:', getEventListeners(debugButton));
                console.log('📝 debugInput のイベントリスナー:', getEventListeners(debugInput));
                console.log('📝 document のイベントリスナー:', getEventListeners(document));
                console.groupEnd();
            } else {
                console.log('⚠️ getEventListeners は Chrome DevTools でのみ利用可能です');
            }
        }, 1000);

        // コンソールでの使用例を表示
        console.log(`
🔍 === getEventListeners 使用例 ===

以下のコマンドをConsoleで試してください:

1. 特定要素のイベント一覧:
   getEventListeners(document.getElementById('debugButton'))

2. 各イベントタイプの詳細確認:
   getEventListeners(document.getElementById('debugButton')).click

3. イベントハンドラの詳細情報:
   getEventListeners(document.getElementById('debugButton')).click[0]

4. 関数の内容を確認:
   getEventListeners(document.getElementById('debugButton')).click[0].listener.toString()
        `);
    </script>
</body>
</html>

プロダクション環境でのイベントリスナー管理

getEventListenersは非標準のため、プロダクション環境では独自の管理システムを構築することが推奨されます:

/**
 * イベントリスナー管理クラス
 * プロダクション環境での使用に適したイベントリスナー追跡・管理システム
 */
class EventListenerManager {
    constructor() {
        this.listeners = new Map(); // element -> { eventType -> listeners[] }
        this.debugMode = false;
    }

    /**
     * イベントリスナーを追加し、管理対象として登録
     */
    addEventListener(element, eventType, handler, options = {}) {
        // 実際のイベントリスナーを登録
        element.addEventListener(eventType, handler, options);

        // 管理用データ構造に追加
        if (!this.listeners.has(element)) {
            this.listeners.set(element, new Map());
        }

        const elementListeners = this.listeners.get(element);
        if (!elementListeners.has(eventType)) {
            elementListeners.set(eventType, []);
        }

        elementListeners.get(eventType).push({
            handler,
            options,
            addedAt: new Date(),
            stackTrace: this.debugMode ? new Error().stack : null
        });

        if (this.debugMode) {
            console.log(`📝 イベントリスナー追加: ${eventType}`, { element, handler, options });
        }
    }

    /**
     * イベントリスナーを削除し、管理対象からも除外
     */
    removeEventListener(element, eventType, handler, options = {}) {
        // 実際のイベントリスナーを削除
        element.removeEventListener(eventType, handler, options);

        // 管理用データ構造からも削除
        const elementListeners = this.listeners.get(element);
        if (elementListeners && elementListeners.has(eventType)) {
            const handlers = elementListeners.get(eventType);
            const index = handlers.findIndex(item => item.handler === handler);

            if (index !== -1) {
                handlers.splice(index, 1);

                if (handlers.length === 0) {
                    elementListeners.delete(eventType);
                }

                if (elementListeners.size === 0) {
                    this.listeners.delete(element);
                }

                if (this.debugMode) {
                    console.log(`🗑️ イベントリスナー削除: ${eventType}`, { element, handler });
                }
            }
        }
    }

    /**
     * 特定要素のイベントリスナー一覧を取得
     */
    getEventListeners(element) {
        const elementListeners = this.listeners.get(element);
        if (!elementListeners) return {};

        const result = {};
        for (const [eventType, handlers] of elementListeners) {
            result[eventType] = handlers.map(item => ({
                handler: item.handler,
                options: item.options,
                addedAt: item.addedAt,
                functionName: item.handler.name || 'anonymous'
            }));
        }

        return result;
    }

    /**
     * 全要素のイベントリスナー統計情報を取得
     */
    getStatistics() {
        let totalElements = 0;
        let totalListeners = 0;
        const eventTypeCount = {};

        for (const [element, elementListeners] of this.listeners) {
            totalElements++;
            for (const [eventType, handlers] of elementListeners) {
                totalListeners += handlers.length;
                eventTypeCount[eventType] = (eventTypeCount[eventType] || 0) + handlers.length;
            }
        }

        return {
            totalElements,
            totalListeners,
            eventTypeCount,
            averageListenersPerElement: totalElements > 0 ? (totalListeners / totalElements).toFixed(2) : 0
        };
    }

    /**
     * 特定要素の全イベントリスナーを削除
     */
    removeAllListeners(element) {
        const elementListeners = this.listeners.get(element);
        if (!elementListeners) return;

        for (const [eventType, handlers] of elementListeners) {
            handlers.forEach(item => {
                element.removeEventListener(eventType, item.handler, item.options);
            });
        }

        this.listeners.delete(element);

        if (this.debugMode) {
            console.log(`🧹 要素の全イベントリスナーを削除`, { element });
        }
    }

    /**
     * 全てのイベントリスナーを削除(クリーンアップ)
     */
    removeAll() {
        for (const element of this.listeners.keys()) {
            this.removeAllListeners(element);
        }

        console.log('🧹 全てのイベントリスナーをクリーンアップしました');
    }

    /**
     * デバッグモードの切り替え
     */
    setDebugMode(enabled) {
        this.debugMode = enabled;
        console.log(`🔍 デバッグモード: ${enabled ? 'ON' : 'OFF'}`);
    }

    /**
     * メモリリークの可能性がある古いリスナーを検出
     */
    detectPotentialLeaks(maxAgeHours = 24) {
        const now = new Date();
        const potentialLeaks = [];

        for (const [element, elementListeners] of this.listeners) {
            // 要素がDOMから削除されているかチェック
            if (!document.contains(element)) {
                potentialLeaks.push({
                    element,
                    reason: 'Element not in DOM',
                    listenerCount: Array.from(elementListeners.values()).reduce((sum, handlers) => sum + handlers.length, 0)
                });
                continue;
            }

            // 古いリスナーをチェック
            for (const [eventType, handlers] of elementListeners) {
                handlers.forEach(item => {
                    const ageHours = (now - item.addedAt) / (1000 * 60 * 60);
                    if (ageHours > maxAgeHours) {
                        potentialLeaks.push({
                            element,
                            eventType,
                            handler: item.handler,
                            age: `${ageHours.toFixed(1)} hours`,
                            reason: 'Old listener'
                        });
                    }
                });
            }
        }

        return potentialLeaks;
    }
}

// グローバルインスタンスの作成
const eventManager = new EventListenerManager();

// 使用例
const button = document.createElement('button');
button.textContent = 'テストボタン';
document.body.appendChild(button);

function handleClick() {
    console.log('ボタンがクリックされました');
}

// 管理された方法でイベントリスナーを追加
eventManager.addEventListener(button, 'click', handleClick);

// デバッグ情報の確認
console.log('登録済みイベントリスナー:', eventManager.getEventListeners(button));
console.log('統計情報:', eventManager.getStatistics());

// メモリリーク検出の例
setTimeout(() => {
    const leaks = eventManager.detectPotentialLeaks();
    if (leaks.length > 0) {
        console.warn('⚠️ 潜在的なメモリリークを検出:', leaks);
    }
}, 5000);

バブリングとキャプチャリングの仕組みとデリゲーションの活用

イベントの伝播メカニズムを理解することで、より効率的で保守性の高いコードが書けるようになります。

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

  1. キャプチャフェーズ: ルート要素から対象要素へ向かってイベントが伝播
  2. ターゲットフェーズ: 実際にイベントが発生した要素で処理
  3. バブリングフェーズ: 対象要素からルート要素へ向かってイベントが伝播
<div class="propagation-demo">
  <h3>🔄 イベント伝播の仕組み(バブリング・キャプチャリング)</h3>

  <div class="phase-indicator">
    <h4>📋 イベント伝播の流れ</h4>
    <div>1️⃣ <strong>キャプチャフェーズ</strong>: Document → HTML → Body → Div → Button</div>
    <div>2️⃣ <strong>ターゲットフェーズ</strong>: Button(イベント発生元)で処理実行</div>
    <div>3️⃣ <strong>バブリングフェーズ</strong>: Button → Div → Body → HTML → Document</div>
  </div>

  <div class="event-tree" id="documentLevel">
    <strong>📄 Document Level</strong>
    <div class="parent-div" id="parentDiv">
      <strong>📁 Parent Div</strong>
      <div class="child-div" id="childDiv">
        <strong>📂 Child Div</strong>
        <br>
        <button class="target-button" id="targetButton">🎯 クリックしてください</button>
      </div>
    </div>
  </div>

  <div class="log-container">
    <div class="log-section">
      <div class="log-header capture-log">⬇️ キャプチャフェーズ</div>
      <div class="log-content" id="captureLog"></div>
    </div>
    <div class="log-section">
      <div class="log-header bubble-log">⬆️ バブリングフェーズ</div>
      <div class="log-content" id="bubbleLog"></div>
    </div>
  </div>

  <div style="text-align: center; margin: 20px 0;">
    <button onclick="clearLogs()" style="padding: 8px 16px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">ログクリア</button>
  </div>
</div>

<!-- イベントデリゲーションのデモ -->
<div class="propagation-demo" style="border-top: 2px solid #ddd; margin-top: 40px; padding-top: 40px;">
  <h3>🎯 イベントデリゲーションの活用</h3>

  <div style="background-color: #e7f3ff; padding: 15px; border-radius: 4px; margin: 20px 0;">
    <h4>💡 イベントデリゲーションとは?</h4>
    <p>子要素ではなく<strong>親要素にイベントリスナーを登録</strong>し、イベントバブリングを利用して子要素のイベントを処理する手法です。</p>

    <h4>🚀 メリット:</h4>
    <ul>
      <li><strong>パフォーマンス向上</strong>: 大量の子要素に個別のリスナーを登録する必要がない</li>
      <li><strong>動的要素対応</strong>: 後から追加される要素も自動的にイベント処理対象になる</li>
      <li><strong>メモリ効率</strong>: イベントリスナーの数を大幅に削減</li>
      <li><strong>保守性向上</strong>: 一箇所でイベント処理を管理</li>
    </ul>
  </div>

  <div style="display: flex; gap: 20px; margin: 20px 0;">
    <div style="flex: 1;">
      <h4>❌ 非効率な方法(個別登録)</h4>
      <div id="inefficientList" style="border: 1px solid #dc3545; padding: 15px; border-radius: 4px; background-color: #f8d7da;">
        <button onclick="addInefficientItem()">❌ アイテム追加(非効率)</button>
        <div id="inefficientContainer"></div>
        <div style="font-size: 12px; margin-top: 10px;">
          イベントリスナー数: <span id="inefficientCount">0</span>
        </div>
      </div>
    </div>

    <div style="flex: 1;">
      <h4>✅ 効率的な方法(デリゲーション)</h4>
      <div id="efficientList" style="border: 1px solid #28a745; padding: 15px; border-radius: 4px; background-color: #d4edda;">
        <button onclick="addEfficientItem()">✅ アイテム追加(効率的)</button>
        <div id="efficientContainer"></div>
        <div style="font-size: 12px; margin-top: 10px;">
          イベントリスナー数: <span id="efficientCount">1</span>
        </div>
      </div>
    </div>
  </div>

  <div id="delegationLog" style="border: 1px solid #ccc; height: 150px; overflow-y: auto; padding: 10px; font-family: monospace; font-size: 12px; background-color: #f8f9fa;"></div>
</div>

<!-- 高度なイベントデリゲーション例 -->
<div class="propagation-demo" style="border-top: 2px solid #ddd; margin-top: 40px; padding-top: 40px;">
  <h3>🎛️ 高度なイベントデリゲーション活用例</h3>

  <div style="background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin: 20px 0;">
    <h4>🔥 実務で使える高度なテクニック</h4>
    <ul>
      <li><strong>複数イベント対応</strong>: クリック、ダブルクリック、右クリックを一括処理</li>
      <li><strong>条件分岐処理</strong>: data属性やクラス名による動的な処理分岐</li>
      <li><strong>ネストした要素対応</strong>: closest()メソッドを使った柔軟な要素検索</li>
      <li><strong>パフォーマンス最適化</strong>: イベント頻度制限(throttle/debounce)</li>
    </ul>
  </div>

  <div id="advancedDemo" style="border: 2px solid #007bff; padding: 20px; border-radius: 8px; background-color: #f8f9fa;">
    <h4>📋 タスク管理デモ(高度なデリゲーション)</h4>

    <div style="margin: 15px 0;">
      <input type="text" id="taskInput" placeholder="新しいタスクを入力" style="padding: 8px; width: 200px; border: 1px solid #ddd; border-radius: 4px;">
      <select id="taskPriority" style="padding: 8px; margin-left: 5px; border: 1px solid #ddd; border-radius: 4px;">
        <option value="low">低優先度</option>
        <option value="medium" selected>中優先度</option>
        <option value="high">高優先度</option>
      </select>
      <button onclick="addTask()" style="padding: 8px 16px; margin-left: 5px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">追加</button>
    </div>

    <div id="taskContainer" style="margin: 20px 0;"></div>

    <div style="font-size: 12px; color: #6c757d;">
      💡 操作方法:
      <strong>左クリック</strong>=完了切替,
      <strong>ダブルクリック</strong>=編集,
      <strong>右クリック</strong>=削除確認,
      <strong>ドラッグ</strong>=並び替え
    </div>
  </div>

  <div id="advancedLog" style="border: 1px solid #ccc; height: 200px; overflow-y: auto; padding: 10px; font-family: monospace; font-size: 12px; background-color: white; margin-top: 20px;"></div>
</div>
.propagation-demo {
  font-family: Arial, sans-serif;
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
}
.event-tree {
  border: 3px solid #007bff;
  padding: 20px;
  margin: 20px;
  background-color: #e3f2fd;
  border-radius: 8px;
}
.parent-div {
  border: 2px solid #28a745;
  padding: 15px;
  margin: 10px;
  background-color: #d4edda;
  border-radius: 6px;
}
.child-div {
  border: 2px solid #ffc107;
  padding: 10px;
  margin: 10px;
  background-color: #fff3cd;
  border-radius: 4px;
  cursor: pointer;
}
.target-button {
  background-color: #dc3545;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}
.log-container {
  display: flex;
  gap: 20px;
  margin: 20px 0;
}
.log-section {
  flex: 1;
  border: 1px solid #ddd;
  border-radius: 4px;
  background-color: #f8f9fa;
}
.log-header {
  padding: 10px;
  font-weight: bold;
  border-bottom: 1px solid #ddd;
}
.capture-log {
  background-color: #d1ecf1;
}
.bubble-log {
  background-color: #f8d7da;
}
.log-content {
  height: 200px;
  overflow-y: auto;
  padding: 10px;
  font-family: monospace;
  font-size: 12px;
}
.phase-indicator {
  margin: 20px 0;
  padding: 15px;
  background-color: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
}
const captureLog = document.getElementById("captureLog");
const bubbleLog = document.getElementById("bubbleLog");
function logToCapture(message) {
const time = new Date().toLocaleTimeString();
captureLog.innerHTML += `<div style="color: #0c5460;">[${time}] ${message}</div>`;
captureLog.scrollTop = captureLog.scrollHeight;
}
function logToBubble(message) {
const time = new Date().toLocaleTimeString();
bubbleLog.innerHTML += `<div style="color: #721c24;">[${time}] ${message}</div>`;
bubbleLog.scrollTop = bubbleLog.scrollHeight;
}
function clearLogs() {
captureLog.innerHTML = "";
bubbleLog.innerHTML = "";
}
// 各要素に対してキャプチャフェーズとバブリングフェーズの両方のリスナーを設定
const elements = [
{ element: document, name: "Document" },
{
element: document.getElementById("documentLevel"),
name: "Document Level Div"
},
{ element: document.getElementById("parentDiv"), name: "Parent Div" },
{ element: document.getElementById("childDiv"), name: "Child Div" },
{ element: document.getElementById("targetButton"), name: "Target Button" }
];
elements.forEach(({ element, name }) => {
// キャプチャフェーズ(第3引数をtrueまたは{capture: true})
element.addEventListener(
"click",
function (e) {
logToCapture(`📍 ${name} - キャプチャフェーズで検知`);
},
true
); // capture: true
// バブリングフェーズ(デフォルト)
element.addEventListener(
"click",
function (e) {
logToBubble(`📍 ${name} - バブリングフェーズで検知`);
},
false
); // capture: false (デフォルト)
});
// ターゲット要素での特別な処理
document.getElementById("targetButton").addEventListener("click", function (e) {
logToBubble(`🎯 ターゲット要素で直接処理実行`);
// イベントの詳細情報をログに表示
setTimeout(() => {
logToBubble(
`📊 イベント詳細: target=${e.target.tagName}, currentTarget=${e.currentTarget.tagName}`
);
logToBubble(`📊 座標: clientX=${e.clientX}, clientY=${e.clientY}`);
}, 50);
});
let inefficientItemCount = 0;
let efficientItemCount = 0;
let totalInefficientListeners = 0;
const delegationLog = document.getElementById("delegationLog");
function logDelegation(message) {
const time = new Date().toLocaleTimeString();
delegationLog.innerHTML =
`<div>[${time}] ${message}</div>` + delegationLog.innerHTML;
}
// ❌ 非効率な方法: 各アイテムに個別にイベントリスナーを登録
function addInefficientItem() {
inefficientItemCount++;
const container = document.getElementById("inefficientContainer");
const item = document.createElement("div");
item.innerHTML = `
<div style="margin: 5px 0; padding: 8px; border: 1px solid #ddd; border-radius: 4px; background-color: white;">
<span>アイテム ${inefficientItemCount}</span>
<button class="delete-btn" style="margin-left: 10px; padding: 2px 6px; background-color: #dc3545; color: white; border: none; border-radius: 2px; cursor: pointer;">削除</button>
<button class="edit-btn" style="margin-left: 5px; padding: 2px 6px; background-color: #007bff; color: white; border: none; border-radius: 2px; cursor: pointer;">編集</button>
</div>
`;
// 各ボタンに個別のイベントリスナーを登録(非効率)
const deleteBtn = item.querySelector(".delete-btn");
const editBtn = item.querySelector(".edit-btn");
deleteBtn.addEventListener("click", function () {
item.remove();
totalInefficientListeners -= 2; // 削除されたリスナー数を減らす
updateInefficientCount();
logDelegation(`❌ 非効率: アイテム ${inefficientItemCount} 削除`);
});
editBtn.addEventListener("click", function () {
const span = item.querySelector("span");
const newText = prompt("新しい名前を入力してください:", span.textContent);
if (newText) {
span.textContent = newText;
logDelegation(`❌ 非効率: アイテム編集 -> "${newText}"`);
}
});
totalInefficientListeners += 2; // 2つのリスナーを追加
updateInefficientCount();
container.appendChild(item);
logDelegation(
`❌ 非効率: 新アイテム追加(${totalInefficientListeners}個のリスナー)`
);
}
function updateInefficientCount() {
document.getElementById(
"inefficientCount"
).textContent = totalInefficientListeners;
}
// ✅ 効率的な方法: イベントデリゲーションを使用
const efficientContainer = document.getElementById("efficientContainer");
// 親要素に1つだけイベントリスナーを登録
efficientContainer.addEventListener("click", function (e) {
// クリックされた要素が削除ボタンの場合
if (e.target.classList.contains("delete-btn")) {
const item = e.target.closest("div");
const itemName = item.querySelector("span").textContent;
item.remove();
logDelegation(`✅ 効率的: ${itemName} を削除(デリゲーション使用)`);
}
// クリックされた要素が編集ボタンの場合
else if (e.target.classList.contains("edit-btn")) {
const span = e.target.closest("div").querySelector("span");
const newText = prompt("新しい名前を入力してください:", span.textContent);
if (newText) {
const oldText = span.textContent;
span.textContent = newText;
logDelegation(
`✅ 効率的: "${oldText}" -> "${newText}"(デリゲーション使用)`
);
}
}
});
function addEfficientItem() {
efficientItemCount++;
const item = document.createElement("div");
item.innerHTML = `
<div style="margin: 5px 0; padding: 8px; border: 1px solid #ddd; border-radius: 4px; background-color: white;">
<span>アイテム ${efficientItemCount}</span>
<button class="delete-btn" style="margin-left: 10px; padding: 2px 6px; background-color: #dc3545; color: white; border: none; border-radius: 2px; cursor: pointer;">削除</button>
<button class="edit-btn" style="margin-left: 5px; padding: 2px 6px; background-color: #007bff; color: white; border: none; border-radius: 2px; cursor: pointer;">編集</button>
</div>
`;
// イベントリスナーは追加しない!親要素のデリゲーションで処理
efficientContainer.appendChild(item);
logDelegation(`✅ 効率的: 新アイテム追加(リスナー数は常に1個)`);
}
// 初期ログ
logDelegation("🚀 イベントデリゲーションのデモを開始しました");
logDelegation(
"💡 両方の方法でアイテムを追加して、効率の違いを確認してください"
);
const taskContainer = document.getElementById("taskContainer");
const taskInput = document.getElementById("taskInput");
const taskPriority = document.getElementById("taskPriority");
const advancedLog = document.getElementById("advancedLog");
let taskIdCounter = 0;
function logAdvanced(message) {
const time = new Date().toLocaleTimeString();
advancedLog.innerHTML =
`<div>[${time}] ${message}</div>` + advancedLog.innerHTML;
}
// 高度なイベントデリゲーション: 複数イベントを一括処理
taskContainer.addEventListener("click", handleTaskEvent);
taskContainer.addEventListener("dblclick", handleTaskEvent);
taskContainer.addEventListener("contextmenu", handleTaskEvent);
taskContainer.addEventListener("dragstart", handleTaskEvent);
taskContainer.addEventListener("dragend", handleTaskEvent);
function handleTaskEvent(e) {
const taskItem = e.target.closest(".task-item");
if (!taskItem) return;
const taskId = taskItem.dataset.taskId;
const taskText = taskItem.querySelector(".task-text").textContent;
const priority = taskItem.dataset.priority;
switch (e.type) {
case "click":
// 完了状態の切り替え
e.preventDefault();
toggleTaskCompletion(taskItem);
logAdvanced(`📝 クリック: "${taskText}" の完了状態を切り替え`);
break;
case "dblclick":
// タスクの編集
e.preventDefault();
editTask(taskItem);
logAdvanced(`✏️ ダブルクリック: "${taskText}" の編集開始`);
break;
case "contextmenu":
// 右クリックで削除確認
e.preventDefault();
if (confirm(`「${taskText}」を削除しますか?`)) {
deleteTask(taskItem);
logAdvanced(`🗑️ 右クリック: "${taskText}" を削除`);
}
break;
case "dragstart":
// ドラッグ開始
e.dataTransfer.setData("text/plain", taskId);
taskItem.style.opacity = "0.5";
logAdvanced(`🔄 ドラッグ開始: "${taskText}"`);
break;
case "dragend":
// ドラッグ終了
taskItem.style.opacity = "1";
logAdvanced(`🔄 ドラッグ終了: "${taskText}"`);
break;
}
}
// ドロップ処理
taskContainer.addEventListener("dragover", function (e) {
e.preventDefault();
});
taskContainer.addEventListener("drop", function (e) {
e.preventDefault();
const draggedTaskId = e.dataTransfer.getData("text/plain");
const draggedTask = document.querySelector(
`[data-task-id="${draggedTaskId}"]`
);
const dropTarget = e.target.closest(".task-item");
if (dropTarget && dropTarget !== draggedTask) {
const container = dropTarget.parentNode;
const nextSibling = dropTarget.nextSibling;
container.insertBefore(draggedTask, nextSibling);
logAdvanced(`🔄 ドロップ: タスクの並び順を変更`);
}
});
function addTask() {
const text = taskInput.value.trim();
if (!text) {
alert("タスクを入力してください");
return;
}
taskIdCounter++;
const priority = taskPriority.value;
const priorityColors = {
low: "#28a745",
medium: "#ffc107",
high: "#dc3545"
};
const taskItem = document.createElement("div");
taskItem.className = "task-item";
taskItem.dataset.taskId = taskIdCounter;
taskItem.dataset.priority = priority;
taskItem.draggable = true;
taskItem.style.cssText = `
border: 2px solid ${priorityColors[priority]};
margin: 8px 0;
padding: 12px;
border-radius: 6px;
background-color: white;
cursor: pointer;
transition: all 0.2s ease;
`;
taskItem.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between;">
<span class="task-text" style="flex: 1;">${text}</span>
<span class="task-priority" style="padding: 2px 8px; border-radius: 12px; font-size: 11px; color: white; background-color: ${
priorityColors[priority]
};">
${priority.toUpperCase()}
</span>
</div>
`;
taskContainer.appendChild(taskItem);
taskInput.value = "";
logAdvanced(`➕ 新タスク追加: "${text}" (${priority}優先度)`);
}
function toggleTaskCompletion(taskItem) {
const isCompleted = taskItem.classList.contains("completed");
if (isCompleted) {
taskItem.classList.remove("completed");
taskItem.style.opacity = "1";
taskItem.style.textDecoration = "none";
} else {
taskItem.classList.add("completed");
taskItem.style.opacity = "0.6";
taskItem.style.textDecoration = "line-through";
}
}
function editTask(taskItem) {
const taskText = taskItem.querySelector(".task-text");
const currentText = taskText.textContent;
const newText = prompt("タスクを編集:", currentText);
if (newText && newText !== currentText) {
taskText.textContent = newText;
logAdvanced(`✏️ タスク編集: "${currentText}" → "${newText}"`);
}
}
function deleteTask(taskItem) {
taskItem.remove();
}
// Enterキーでタスク追加
taskInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
addTask();
}
});
// 初期化ログ
logAdvanced("🚀 高度なイベントデリゲーションデモを開始");
logAdvanced("💡 タスクを追加して、様々な操作を試してください");

実際の表示

See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.

よくある質問(FAQ)

addEventListenerを使えば、onclickはもう使わなくてもいいの?

はい、基本的にはその通りです。onclickのようなインラインイベントハンドラは、HTMLとJavaScriptが混在してコードが読みにくくなる、一つのイベントに複数の関数を登録できないといったデメリットがあります。モダンなWeb開発では、コードの管理や拡張性を考慮して、addEventListenerを使うのがベストプラクティスとされています。

ReactやVue.jsなどのフレームワークでもこの知識は役立ちますか?

もちろんです。ReactやVue.jsは独自のイベントハンドリングの仕組み(仮想DOMなど)を持っていますが、その根底にあるのはJavaScriptのネイティブなイベントです。例えば、ReactのonClickは、内部的にaddEventListenerを効率的に利用しています。JavaScriptのイベントの仕組みを深く理解しておくことは、フレームワークのイベントハンドリングがどのように機能しているかを理解する上で非常に重要です。

イベントリスナーを削除しないとどうなりますか?

イベントリスナーを削除しないと、ページ上の要素がなくなった後もメモリ上にリスナーが残り続ける**「メモリリーク」**が発生する可能性があります。特に、シングルページアプリケーション(SPA)のように、頻繁にDOM要素の追加や削除が行われるサイトでは、メモリリークがパフォーマンス低下の大きな原因になり得ます。不要になったリスナーは、removeEventListenerで適切に削除することが重要です。

preventDefault()stopPropagation()はどういうときに使いますか?

  • event.preventDefault(): イベントのデフォルトの動作(ブラウザが元々持っている動作)を無効にしたいときに使います。例えば、リンクをクリックしたときの画面遷移を防いだり、フォーム送信時のページリロードを止めたりする際に利用します。
  • event.stopPropagation(): イベントの伝播(バブリングやキャプチャリング)を途中で止めたいときに使います。親要素にも同じ種類のイベントリスナーが設定されている場合に、子要素でのイベントが親要素に伝わらないようにしたい場合などに有効です。

どのイベントを使えばいいか迷ったら?

まずは「ユーザーがどのような操作をするか?」を考え、それに最も近いイベントを選ぶのが基本です。

  • ボタンのクリックclick
  • テキスト入力のたびに処理input
  • フォーム送信submit
  • キー入力keydownまたはkeyup

迷ったら、そのイベントがいつ、どのくらい発火するのかをコンソールにログを出力して確認してみるのも良い方法です。

まとめ

この記事では、JavaScriptのイベントリスナーについて基本から実践的な活用方法まで詳しく解説してきました。

イベントリスナーは、ユーザーの操作を検知してWebサイトにインタラクティブな機能を実装するための重要な技術です。単なるボタンクリックから、複雑なドラッグ&ドロップ機能まで、現代のWeb開発には欠かせない存在といえるでしょう。

まず押さえておきたいのは、addEventListenerが現在の標準的な書き方だということです。古いonclick記法と比べて複数のイベントリスナーを登録できるなど、柔軟性と保守性の面で大きなメリットがあります。また、oncepassiveといったオプションを活用することで、パフォーマンスの最適化も図れます。

実際の開発では、マウスイベント、キーボードイベント、フォームイベントなど、様々なイベントタイプを適材適所で使い分けることが重要です。特にinputchangeの違い、DOMContentLoadedloadの使い分けなど、似ているようで異なるイベントの特徴を理解することで、より効果的なコードが書けるようになります。

さらに、モバイル対応を考慮したタッチイベントの実装や、HTML5 Drag and Drop APIを使った直感的なUI設計も、今や必須のスキルです。

そして何より大切なのは、適切なイベントリスナーの管理です。removeEventListenerを使った適切な削除、Chrome DevToolsでのgetEventListenersを使ったデバッグ、そしてイベントデリゲーションによるパフォーマンス最適化は、プロフェッショナルな開発者として身につけるべき技術です。

重要ポイント

  • addEventListenerを使用onclickより柔軟で保守性が高い
  • 第3引数のオプション活用oncepassiveでパフォーマンス向上
  • イベントタイプの使い分けinput vs changeDOMContentLoaded vs load
  • 適切なリスナー管理:メモリリーク防止のためremoveEventListenerで削除
  • イベントデリゲーション:大量の子要素を効率的に管理
  • デバッグ技術の習得getEventListenersで問題の早期発見

この記事の内容をしっかりと理解し、実際にコードを書いて試すことで、JavaScriptイベントリスナーの真の実力を発揮できるはずです。ぜひブックマークして、開発の現場で活用してください。これからのWeb開発がより楽しく、そして効率的になることを願っています!

タイトルとURLをコピーしました