🫨

JavaScriptのeオブジェクトって何なん!?

に公開

onClick・onChangeで出てくるアレの正体 判明させましょう( `ー´)ノ

はじめに

JavaScriptを学び始めると、必ずこんなコードに出会いますよね。

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

この謎の e って一体何者なんでしょうか。別に理解しなくても実装はできますが、知識がついてきただけにしっかりと言語化できるように調べました✨

「eはイベントオブジェクトです」って言われても、「eだけに、えぇ?」っていまいちピンと来ない。
誰が作ってるの?何が入ってるの?いつ使うの?

この記事では、そんな疑問を初学者の方でもなるべく、わかりやすく理解できるように、
ブラウザの仕組みから丁寧に解説していこうと思います!

まずはブラウザの仕組みを知ろう!!

eオブジェクトの正体を知るには、まずブラウザがどんな構造になっているかを理解する必要があります。

ブラウザは4つの主要パーツで動いている

ブラウザって、実は4つの重要なパーツで構成されているんです。

1. HTTPクライアント

サーバーからHTML、CSS、JavaScriptファイルを取得してくる役割です。
サーバーにこのファイルちょうだい~ってお願いしてくれる役割の人ですね✨

2. レンダリングエンジン

サーバーから届いたHTMLの記述を一つずつ読み取って、画面として組み立てたり、CSSの指示通りに見た目を整える役割です。
設計図を読んで、家を建てて、見た目を整える人みたいなイメージ。

3. JavaScriptエンジン

JavaScriptのコードを実行する役割です。V8とかSpiderMonkeyとか聞いたことありませんか?
これがJavaScriptエンジンです。
こいつは、私の中では「家の中にいて、指示された計算や翻訳をひたすらこなす、頭のいい学者さん」のようなイメージとして考えています。

4. WebAPIs

そして今回の主役がこれ!!WebAPIsです!!
家の外にある便利な機能を、家の中で使えるようにしてくれる人や、家の中で色々と便利に働いてくれる人(例えば、外の天気を教えてくれたり、時間を計ってくれたりするお手伝いさん)のようなイメージです。

JavaScriptエンジンは実は何もできない

ここで、ちょっと衝撃的な事実なんですが、
JavaScriptエンジンって、実は純粋な計算しかできないんです!!

// これはJavaScriptエンジンだけでできる
let x = 1 + 1;
function add(a, b) { 
  return a + b; 
}
const name = "太郎";

でも、こういうのはどうでしょう。

// これは全部JavaScriptエンジン「だけ」ではできない
console.log("Hello");  // 画面に出力
document.getElementById("btn");  // HTMLを触る
setTimeout(() => {}, 1000);  // 時間を測る
fetch("https://api.example.com");  // ネットワーク通信

なぜできないのか。

JavaScriptエンジンは「計算マシン」であって、画面のことも、ネットワークのことも、ファイルシステムのことも、ユーザーの入力のことも知らないからです。

つまり、外の世界とのやり取りができないんですね。

WebAPIsが「外の世界への窓口」

そこで登場するのがWebAPIsです。

WebAPIsは、ブラウザが提供する「外の世界への窓口」なんです。
JavaScriptエンジンと外の世界をつなぐ橋渡し役ですね。

[JavaScriptエンジン] ←→ [WebAPIs] ←→ [外の世界]
   (計算だけ)           (橋渡し)    (画面、ネット等)

WebAPIsには色々な種類があります。

  • DOM API(HTMLを操作する)
  • Fetch API(ネットワーク通信する)
  • Console API(開発者ツールに出力する)
  • Timer API(時間を測る)
  • Web Storage API(データを保存する)

重要なのは、これらのAPIはブラウザに最初から組み込まれているということです。
サーバーから何かをダウンロードして作られるものではありません。
ブラウザを起動した瞬間から使える状態になっているんです。
※知れば知るほど、ブラウザすげ~って最近、思っています(^^)

DOM APIとレンダリングエンジンの関係

eオブジェクトの話をする前に、もう一つ理解しておきたいことがあります。
それが、DOM APIとレンダリングエンジンの関係です。

HTMLは文字列、DOMはオブジェクト

サーバーから送られてくるHTMLは、ただの「文字」が書かれた紙。
でも、そのままだとJavaScriptは「どこに何があるか」が分かりません。

<!-- これはただの文字列 -->
<button id="myBtn">クリック</button>

そこでレンダリングエンジンが、その紙の内容を読み取って、パソコンの作業用スペース(メモリ)に「すぐに触れる実体」として組み立て直します。これがDOMツリーの私のイメージです。

Document
  └─ button (Element)
       ├─ id: "myBtn"
       ├─ textContent: "クリック"
       └─ その他のプロパティ...

つまり、「ただの文字情報」が「実際に操作できる模型」に変わった状態なんですね(^^)

DOM APIがDOMツリーへのアクセス手段を提供

JavaScriptがこのDOMツリーを操作できるように、DOM APIが様々なメソッドを提供しています。

// DOM APIを使ってDOMツリーにアクセス
const btn = document.getElementById("myBtn");
btn.textContent = "押してね";
btn.style.color = "red";

つまり、こういう関係性だと思っています。

[レンダリングエンジン]
  HTMLを解析 → DOMツリー作成
  
[DOM API]
  DOMツリーを操作する手段を提供
  
[JavaScript]
  DOM APIを使ってDOMツリーを操作

eオブジェクトの正体

さあ、ここまで来たら、本題のeオブジェクトの正体を見ていきましょう✨

eオブジェクトは、DOM APIが作り出すEventオブジェクトなんです。

button.addEventListener("click", (e) => {
  // このeは誰が作った?
  // 答え:DOM API(ブラウザ)が作った!
  console.log(e);
});

eオブジェクトが生まれるまでの流れ

ユーザーがボタンをクリックしてから、eオブジェクトが関数に渡されるまでの流れを見てみましょう。

ステップ1 イベントリスナーの登録

バニラJS(素のJavaScript)の場合

const button = document.getElementById("myBtn");

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

Reactの場合

function App() {
  const handleClick = (e) => {
    console.log(e);
  };

  return (
    <button onClick={handleClick}>
      クリック
    </button>
  );
}

この時点で、ブラウザは「このbutton要素にclickイベントが起きたら、この関数を実行する」と記憶(イベントリスナーの登録)をします。

ステップ2 Eventオブジェクトの中身
クリックされた瞬間、ブラウザは、その時の状況をまとめた「調査報告書」のようなものを作ります。
これが e(Eventオブジェクト)です。

e(Eventオブジェクト)の中身を覗いてみると、こんな情報が詰まっています。

// ブラウザの頭の中(メモリ上)で作られた e オブジェクトのイメージ
{
  type: "click",                // イベントの種類
  target: button#myBtn,         // 実際にクリックされた要素
  currentTarget: button#myBtn,  // イベントリスナーが設定されている要素
  clientX: 312,                 // マウスが押された横の位置
  clientY: 89,                  // マウスが押された縦の位置
  timeStamp: 1234.56,           // ページが開いてから何ミリ秒後に起きたか
  isTrusted: true,              // ユーザー本人が操作したか(スクリプト操作じゃないか)
  altKey: false,                // Altキーを一緒に押していたか
  shiftKey: false,              // Shiftキーを一緒に押していたか
  /* 他にもたくさんの情報が隠されています... */
}

実際にコンソールで見たいのであれば、以下のようにすればいいですね(^^)

button.addEventListener("click", (e) => {
  console.log(e.type);           // 何が起きた? → "click"
  console.log(e.target);         // 誰がクリックされた? → <button id="myBtn">
  console.log(e.clientX);        // 画面のどこで? → 312(横の座標)
  console.log(e.timeStamp);      // いつ起きた? → 1234.56(発生時刻)
});

つまり、しつこいくらい書きますが、eオブジェクトの中には、
「どこで、誰が、いつ、何を」という情報を、ブラウザが自動的にパッケージ化してくれるんです!

ステップ3 eオブジェクトが生まれるまでの舞台裏

では、この「調査報告書(e)」は、どうやって私たちの関数に届くのでしょうか?
ユーザーがボタンをクリックした瞬間の、裏側の動きをもう少し詳しく見てみましょう。

実はブラウザは、画面上のどこかがクリックされるたびに、たとえそこにボタンがなかろうが、リスナー(登録された関数)があろうがなかろうが、「どこが、いつ、どうクリックされたか」を毎回計算しています。

もし「リスナーがある時だけ作る」という仕組みにしようとすると、ブラウザはクリックされた瞬間に「えーっと、ここにリスナーはあったかな…?」と探しに行かなければならず、かえって効率が悪くなるからです。

なので、イメージとしてはこんな感じです。

ユーザーが画面のどこかしらをクリック!

ブラウザ: 「座標(100, 200)でクリック発生!よし、情報をまとめて報告書(eの元データ)を作るぞ!」

ブラウザ: 「さて、この場所(要素)に『クリックされたら教えてね』と予約している関数(リスナー)はあるかな?」

判定、、、、、、

リスナーがある場合: 「あった!よし、作った報告書(e)を引数に入れて、あの関数を実行しよう!」

リスナーがない場合: 「誰も予約してないな。じゃあこの報告書はシュレッダーにポイだ(破棄)。」

つまり、ブラウザが情報を集めて、DOM APIがeオブジェクトを作ってくれる。
そしてブラウザが「この場所で動くように予約された関数(リスナー)」を見つけたら、その関数を呼び出すときに、完成したeオブジェクトを引数として手渡してくれる、というイメージですね(^^)

私は、このイメージがつかめたとたんに、なるほど!そういうことか~ってなりました✨

ここまででeイベントがどういうものか掴めた方は、以降の記事は読まなくて大丈夫です(^^)
せっかくなので、色々と蛇足ですが、使い方なども含めてまとめておこうと思います!

changeイベント(入力確定)でも見ておこう♪

入力フォームでも、ブラウザは一生懸命 e を作っています。
ただし、「素のJS」と「React」では、イベントの名前は同じでも、実行されるタイミングにちょっとした違いがあるんです。ここは押させておくべきですよね✨

バニラJS(素のJavaScript)の場合
素のJSには、主に2つの入力イベントがあります。

  1. change イベント(入力確定!) ユーザーが打ち終わって、**「フォーカスを外した(外をクリックした)瞬間」**にだけ実行されます。
const input = document.getElementById("myInput");

input.addEventListener("change", (e) => {
  // フォーカスを外した瞬間に e が届く
  console.log("入力確定!:", e.target.value);
});
  1. input イベント(入力中!) キーボードを**「1文字打つたび」**に即座に実行されます。
const input = document.getElementById("myInput");

input.addEventListener("input", (e) => {
  // 1文字打つたびに e が届く(リアルタイム)
  console.log("入力中…:", e.target.value);
});

Reactの場合
一方、Reactの onChange は、「1文字入力するたび」に動きます。

function App() {
  const handleChange = (e) => {
    // 1文字打つたびに e が届く!
    console.log("入力中:", e.target.value);
  };

  return (
    <input type="text" onChange={handleChange} />
  );
}

なぜタイミングが違うの?(onInputとの関係)
実は、ブラウザ(WebAPI)が用意しているイベントには、元々この2種類があります。

onInput: 1文字打つたびに発火(リアルタイム)

onChange: フォーカスを外して確定したときに発火(あとでまとめて)

Reactの「親切な魔法」
Reactは、Stateを使ってリアルタイムに画面を書き換えるのが得意なライブラリです。 そのため、Reactチームは**「onChangeという名前のまま、中身をonInput(リアルタイム)のように動くように作り変えちゃえ!」**と、あえて挙動を改造しました。

これを専門用語で 「合成イベント(SyntheticEvent)」 と呼びます。

名前は change ですが、中身はブラウザから届く input イベントの情報をうまく取り込んで、リアルタイムに動くように調整してくれているんです。
親切なんですが、ちょっとややこしく感じてしまいますよね(´;ω;`)

eを受け取らない場合はどうなる?

ここで質問です。こう書いた場合、eオブジェクトは作られるんでしょうか??

バニラJS(素のJavaScript)の場合

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

Reactの場合

function App() {
  // 引数 e を受け取らない書き方
  const handleClick = () => {
    console.log("クリックされた!");
  };

  return (
    <button onClick={handleClick}>
      クリック
    </button>
  );
}

eを受け取っていませんね。この場合、ブラウザはEventオブジェクトを作らないのでしょうか。

答え いいえ、必ず作ります。
上記のステップ3 eオブジェクトが生まれるまでの舞台裏をしっかりと理解できていれば、
当然分かりますよね♪

いつeを使うべきか 実用例で理解する

では、実際にどんなときにeが必要で、どんなときに不要なのか。
具体例で見ていきましょう。

eが必要な場合

1. デフォルト動作を防ぐ

const link = document.getElementById("myLink");

link.addEventListener("click", (e) => {
  e.preventDefault(); // リンクのデフォルト動作(ページ遷移)を防ぐ
  console.log("リンクは無効化されました");
  // 代わりに別の処理を実行
  showModal();
});

2. イベントの伝播(バブリング)を止める

「モーダルの外側(背景)をクリックしたら閉じるようにしたい」という機能をReactで作るとき、避けては通れないのが「イベントの伝播(バブリング)」という現象です。

まずは、バブリングの性質を知っておきましょう。

バブリングは「下(子)から上(親)へ」伝わる
イベントには、「クリックされた要素から、その親要素へ、さらにその親へ……」と、泡(バブル)が浮かんでいくように報告が上がっていく性質があります。これをバブリングと呼びます。

この性質のせいで、モーダルではちょっとした問題が起きます。

【実例】モーダルの中身をクリックしただけなのに閉じちゃう問題
以下のようなモーダルのコードを見てみましょう。

function Modal({ onClose }) {
  return (
    // ① 親:背景(グレーのエリア)。クリックしたら閉じる
    <div 
      className="overlay" 
      onClick={() => {
        console.log("背景がクリックされたので閉じます");
        onClose();
      }}
      style={{ backgroundColor: "rgba(0,0,0,0.5)", padding: "50px" }}
    >
      
      {/* ② 子:モーダルの中身(白いボックス) */}
      <div 
        className="content" 
        onClick={(e) => {
          // ここで何もしないと、イベントが親(①)まで届いてしまう!
          console.log("中身がクリックされた");
        }}
        style={{ backgroundColor: "white", padding: "20px" }}
      >
        <h2>モーダルのタイトル</h2>
        <p>ここをクリックしても閉じてほしくない...</p>
        <button onClick={onClose}>閉じるボタン</button>
      </div>

    </div>
  );
}

このままだと、②の中身(白いボックス)をクリックした瞬間に、イベントが親(①)にまで報告されてしまい、onClose が実行されてモーダルが閉じてしまいます。 「中身を触っただけなのに、背景まで反応しちゃった!」という状態ですね。

ここで e.stopPropagation() の出番!
「親への報告をここでストップして!」とブラウザに命令するのが、eオブジェクトが持つ stopPropagation() メソッドです。

修正後のコード

      {/* ② 子:モーダルの中身(白いボックス) */}
      <div 
        className="content" 
        onClick={(e) => {
          // 💡 これが「防波堤」になる!
          e.stopPropagation(); 
          console.log("親への伝播を止めたので、背景は反応しません");
        }}
      >
        <h2>モーダルのタイトル</h2>
        {/* ... */}
      </div>

仕組みを整理しよう!!
ユーザーの操作: モーダルの中身(子)をクリック
ブラウザ(WebAPI)の動き:まず「子」の onClick を実行
次に「親」の onClick を実行しようとする。(バブリング)

ここで、e.stopPropagation() の効果として、
「子」の実行中にこのメソッドが呼ばれると、ブラウザは「あ、この上の親にはもう報告しなくていいんだな」と理解し、バブリングをその場で中止します。

3. クリック位置を取得する
画面のどこがクリックされたかを知るためには、eオブジェクトに記録された「座標データ」を使います。
例えば、「クリックした場所にキラキラしたエフェクトを出す」とか、「画像の中のどの部分が押されたか判定する」といった時に役立ちます。

const canvas = document.getElementById("myCanvas");

canvas.addEventListener("click", (e) => {
  const x = e.clientX; // ブラウザウィンドウ左端からのX座標
  const y = e.clientY; // ブラウザウィンドウ上端からのY座標
  
  console.log(`クリック位置 (${x}, ${y})`);

});

Reactの場合

function ClickGame() {
  const handleClick = (e) => {
    // 💡 ブラウザが e に詰めてくれた座標データを取り出す
    const x = e.clientX; // ウィンドウの左端からの距離
    const y = e.clientY; // ウィンドウの上端からの距離

    console.log(`クリックされた場所: X=${x}, Y=${y}`);
    
    // この座標を使って、Stateを更新したり描画したりする
    // 例: setEffectPos({ x, y });
  };

  return (
    <div 
      onClick={handleClick} 
      style={{ width: "100%", height: "300px", background: "#f0f0f0" }}
    >
      ここをクリックしてみてね!
    </div>
  );
}

4. 入力値を取得する
検索窓(Input)やセレクトボックスで、ユーザーがいま何を打ったのか、何を選んだのかを知るためにも e は欠かせません。

const input = document.getElementById("searchInput");

input.addEventListener("input", (e) => {
  const value = e.target.value; // 現在の入力値
  console.log(`入力中 ${value}`);
  
  // リアルタイム検索
  if (value.length >= 3) {
    searchAPI(value);
  }
});

Reactでは useState と組み合わせて、1文字打つごとにStateを更新するのが一般的です。

Reactの場合

function SearchBar() {
  const [query, setQuery] = useState("");

  const handleChange = (e) => {
    // 💡 ブラウザが「今この文字が打たれたよ!」と e.target.value で教えてくれる
    const value = e.target.value;
    setQuery(value);

    // リアルタイム検索の実行など
    if (value.length >= 3) {
      console.log(`${value} で検索中...`);
    }
  };

  return (
    <input 
      type="text" 
      value={query} 
      onChange={handleChange} 
      placeholder="検索キーワードを入力"
    />
  );
}

セレクトボックス(Select)の場合
セレクトボックスでも使えます。
選択肢が切り替わった瞬間のデータも、ブラウザがしっかり e に詰めてくれます。

const select = document.getElementById("categorySelect");

select.addEventListener("change", (e) => {
  const selectedValue = e.target.value;
  console.log(`選択されたカテゴリ ${selectedValue}`);
  
  // 選択に応じて表示を変える
  filterItemsByCategory(selectedValue);
});

Reactの場合

function CategoryFilter() {
  const handleSelect = (e) => {
    // 選択された <option> の value が取得できる
    const selectedValue = e.target.value;
    console.log(`カテゴリ「${selectedValue}」が選ばれました`);
  };

  return (
    <select onChange={handleSelect}>
      <option value="all">すべて</option>
      <option value="food">食べ物</option>
      <option value="drink">飲み物</option>
    </select>
  );
}

5. どの要素がクリックされたか特定する
ToDoリストのように、同じようなボタンがたくさん並んでいる場合、「どのボタンが押されたのか」を知る必要がありますよね。

const list = document.getElementById("todoList");

// リスト全体に1つのイベントリスナー(イベント委譲)
list.addEventListener("click", (e) => {
  // クリックされた要素を取得
  if (e.target.classList.contains("delete-btn")) {
    // 削除ボタンがクリックされた
    const todoItem = e.target.closest(".todo-item");
    todoItem.remove();
  } else if (e.target.classList.contains("edit-btn")) {
    // 編集ボタンがクリックされた
    const todoItem = e.target.closest(".todo-item");
    editTodo(todoItem);
  }
});

Reactの場合
Reactでは、マップ(map)で要素を回しながら、各ボタンに「自分のID」を添えて関数を呼び出すのが一般的です。

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "買い物に行く" },
    { id: 2, text: "ゴミ出し" }
  ]);

  const handleDelete = (e, id) => {
    // 💡 e.target を使えば、クリックされたボタンそのものの情報が取れる
    console.log("クリックされたボタンのクラス名:", e.target.className);
    
    // 引数で渡ってきたIDを使って、データを削除する
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} className="todo-item">
          {todo.text}
          <button 
            className="delete-btn" 
            onClick={(e) => handleDelete(e, todo.id)}
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  );
}

eが不要な場合

1. 単にログを出すだけ

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

2. カウンターを増やすだけ

let count = 0;
const counter = document.getElementById("counter");

button.addEventListener("click", () => {
  count++;
  counter.textContent = count;
});

3. モーダルを開くだけ

const modal = document.getElementById("modal");
const openBtn = document.getElementById("openModal");

openBtn.addEventListener("click", () => {
  modal.style.display = "block";
});

4. クラスをトグルするだけ

const menu = document.getElementById("menu");
const menuBtn = document.getElementById("menuBtn");

menuBtn.addEventListener("click", () => {
  menu.classList.toggle("open");
});

5. APIを叩くだけ(入力値は別で取得)

const submitBtn = document.getElementById("submit");
const nameInput = document.getElementById("name");

submitBtn.addEventListener("click", () => {
  // eは使わず、直接inputから値を取得
  const name = nameInput.value;
  
  fetch("/api/users", {
    method: "POST",
    body: JSON.stringify({ name })
  });
});

まとめ eが必要かどうかの判断基準

eが必要な場合 eが不要な場合
デフォルト動作を防ぎたい 単純な処理だけする
イベントの伝播を止めたい 事前に要素への参照がある
クリック位置が知りたい 入力値を別で取得できる
入力値を取得したい 状態管理だけする
どの要素かを特定したい 表示の切り替えだけ
キー入力を判定したい クラスの付け替えだけ
ファイルを取得したい -

基本的な考え方

「イベントに関する情報が必要か?」と自問してみてください。

  • 必要なら → (e) => { }
  • 不要なら → () => { }

他のWebAPIsも見てみよう

eオブジェクトを提供しているDOM API以外にも、色々なWebAPIsがあります。すべて「JavaScriptエンジンができないことを補う」役割です。

Console API

console.log("Hello");
console.error("エラー!");
console.table([{name: "太郎", age: 20}]);

JavaScriptエンジンは「文字列を処理」できますが、画面(開発者ツール)に出力するのはConsole APIの仕事です。

Fetch API

fetch("https://api.example.com/users")
  .then(response => response.json())
  .then(data => console.log(data));

JavaScriptエンジンはネットワーク通信できません。Fetch APIがHTTPリクエストを送り、レスポンスを受け取ります。

Timer API

setTimeout(() => {
  console.log("3秒後");
}, 3000);

setInterval(() => {
  console.log("1秒ごと");
}, 1000);

JavaScriptエンジンは時間を測る機能がありません。Timer APIが時間を測り、時間が来たら関数を呼び出します。

Timer APIの内部動作はこんな感じです。

1. setTimeout()が呼ばれる

2. [Timer API] 「3秒後にこの関数を実行する」と記憶

3. [JavaScriptエンジン] 他のコードを実行し続ける(ブロックされない!)

4. 3秒後

5. [Timer API] 時間が来たことを検知

6. [Timer API] 登録されていた関数を呼び出す

7. [JavaScriptエンジン] 関数を実行

Web Storage API

// データを保存
localStorage.setItem("username", "太郎");

// データを取得
const name = localStorage.getItem("username");
console.log(name); // "太郎"

JavaScriptエンジンはファイルシステムにアクセスできません。Web Storage APIがブラウザのストレージに保存・取得します。

Geolocation API

navigator.geolocation.getCurrentPosition((position) => {
  console.log(position.coords.latitude);  // 緯度
  console.log(position.coords.longitude); // 経度
});

JavaScriptエンジンはGPSにアクセスできません。Geolocation APIがデバイスの位置情報を取得します。

すべてのWebAPIsに共通すること

[JavaScriptエンジン]
  計算しかできない
  ↓ WebAPIを呼び出し
[WebAPIs]
  外の世界とやり取り
  ・DOM API → HTML要素操作
  ・Fetch API → ネットワーク
  ・Timer API → 時間管理
  ・Console API → 画面出力
  ・Storage API → データ保存
  etc...
  ↓ 結果を返す
[JavaScriptエンジン]
  結果を受け取って次の処理

WebAPIsはブラウザに最初から組み込まれているので、いつでも使えます。

感謝

長い記事を読んでいただき、ありがとうございました(´;ω;`)

Discussion