クリックされた要素が特定のボタンなのか、ある要素が指定した親(祖先)の内側にあるのか——DOM を操作していると、こうした「要素の判定」や「祖先の探索」が頻繁に必要になります。JavaScript にはこれらを短く正確に書くためのメソッドが用意されており、代表的なのが Element.closest()・Element.matches()・Node.contains() の3つです。この記事では、それぞれの役割と使い方を実際に動くコードで確認し、最後にこれらが特に威力を発揮するイベント委譲の書き方まで解説します。初心者から中級者の方を対象にしています。
目次
3つのメソッドはそれぞれ何を判定するのか
名前が似ているため混同しやすいのですが、判定する対象と戻り値がはっきり違います。まずは全体像を表で押さえておきましょう。
| メソッド | 調べること | 戻り値 |
|---|---|---|
element.matches(selector) | その要素自身が CSS セレクタに一致するか | true / false |
element.closest(selector) | 自分自身から祖先方向へたどり、最初に一致した要素 | 一致した要素 or null |
node.contains(other) | ある要素が自分の内側(子孫)に含まれるか | true / false |
ざっくり言うと、matches は「この要素は〇〇か?」という自分自身の判定、closest は「一番近い〇〇を探して」という祖先方向の探索、contains は「これは自分の中にあるか?」という包含関係の判定です。順に見ていきます。
matches:要素が条件に合うかを真偽値で調べる
Element.matches() は、その要素が引数に渡した CSS セレクタに一致するかどうかを true / false で返します。CSS で書けるセレクタなら何でも使えるので、タグ名・クラス・属性・状態(:checked など)といった複数の条件をまとめて1回で判定できるのが利点です。
const el = document.querySelector("#save");
// タグ名や属性でまとめて判定できる
el.matches("button"); // button 要素なら true
el.matches(".primary"); // primary クラスを持つなら true
el.matches("button.primary"); // button かつ primary クラスなら true
el.matches("[disabled]"); // disabled 属性があれば true
// 条件分岐にそのまま使える
if (el.matches("button:not([disabled])")) {
console.log("押せるボタンです");
}
classList.contains() でもクラスの有無は調べられますが、matches はクラス・タグ・属性・擬似クラスを組み合わせた複合条件を1行で書ける点が違います。次に紹介する closest と組み合わせると、条件判定の幅がぐっと広がります。
closest:一番近い祖先要素をたどって取得する
Element.closest() は、その要素自身を含めて祖先方向(親→親の親…)へさかのぼり、引数のセレクタに最初に一致した要素を返します。見つからなければ null を返します。「自分自身も対象に含む」点は取り違えやすいので覚えておきましょう。
<article class="card">
<div class="card__body">
<span class="icon">★</span>
</div>
</article>
const icon = document.querySelector(".icon");
// icon 自身から親方向へたどり、最初の .card を返す
icon.closest(".card"); // <article class="card"> が返る
// 自分自身も判定対象。span 自身が一致するのでその span が返る
icon.closest(".icon"); // <span class="icon"> が返る
// たどっても見つからなければ null
icon.closest(".footer"); // null
戻り値が要素または null なので、取得後にそのまま if (target) のように存在チェックできます。深くネストした要素から「その要素が属するカード全体」「所属する行(tr)」などをたどるときに重宝します。
contains:ある要素が内側に含まれるかを調べる
Node.contains() は、引数に渡したノードが自分自身または自分の子孫かどうかを true / false で返します。closest や matches が「上へたどる/自分を判定する」のに対し、contains は「下方向に含んでいるか」を確認するメソッドです。仕様上、自分自身を渡しても true になる点に注意してください。
const menu = document.querySelector(".menu");
const item = document.querySelector(".menu__item");
menu.contains(item); // item が menu の中にあれば true
menu.contains(menu); // 自分自身なので true
menu.contains(document.body); // body は menu の外側なので false
よくあるのが「メニューの外側をクリックしたら閉じる」処理です。クリックされた要素がメニューの内側かどうかを contains で判定できます。
document.addEventListener("click", (event) => {
// クリック地点がメニューの内側でなければ閉じる
if (!menu.contains(event.target)) {
menu.classList.remove("is-open");
}
});
closest が活きるイベント委譲
これらのメソッドが最も役立つ場面のひとつがイベント委譲(イベントデリゲーション)です。リストの各項目のボタンすべてにイベントを個別に設定するのではなく、親要素にリスナーを1つだけ付け、クリックされた要素を closest で判定して処理を振り分けます。要素が後から追加されても親のリスナーがそのまま効くため、動的に増減する UI と相性が良い書き方です。
下のデモは、リストの「完了」ボタンを押すと、その行だけが完了表示に変わる例です。event.target.closest(".done") でクリックされたのがボタン(またはその内側)かを確かめ、続けて closest("li") でそのボタンが属する行をたどっています。JavaScript タブと Preview で動作を確認してみてください。
<ul id="task-list">
<li>牛乳を買う <button class="done">完了</button></li>
<li>返信メールを送る <button class="done">完了</button></li>
<li>記事の下書きを書く <button class="done">完了</button></li>
</ul>
<p id="status">ボタンを押すと、その行だけ「完了」表示になります。</p>
#task-list {
list-style: none;
padding: 0;
max-width: 360px;
}
#task-list li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
margin-bottom: 8px;
border: 1px solid #ddd;
border-radius: 6px;
}
#task-list li.completed {
color: #999;
text-decoration: line-through;
background: #f5f5f5;
}
.done {
border: none;
background: #007bff;
color: #fff;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
}
#status {
color: #555;
font-size: 14px;
}
const list = document.getElementById("task-list");
const status = document.getElementById("status");
// リスト全体で1つだけクリックを監視する(イベント委譲)
list.addEventListener("click", (event) => {
// クリックされた要素が .done ボタンか、その内側かを判定
const button = event.target.closest(".done");
if (!button) return; // ボタン以外の場所なら何もしない
// ボタンの一番近い li 祖先を取得して完了状態にする
const item = button.closest("li");
item.classList.add("completed");
button.textContent = "済";
status.textContent = "「" + item.firstChild.textContent.trim() + "」を完了にしました。";
});
ポイントは event.target.closest(".done") の部分です。event.target は実際にクリックされた要素で、ボタン内にアイコンやテキストがあってもそれらから closest で親のボタンをたどれます。一致しなければ null なので、if (!button) return; でボタン以外のクリックを弾けます。ここで matches を使う書き方もありますが、matches は event.target 自身しか見ないため、ボタンの内側の要素がクリックされると判定を外してしまいます。ボタンの中に別要素が入る可能性を考えると、祖先までたどる closest のほうが安全です。
closest が null を返してエラーになるとき
イベント委譲でよくあるつまずきが、closest の戻り値をチェックせずに続けて使ってしまうケースです。親要素の余白部分などボタン以外がクリックされると closest は null を返し、その null に対してプロパティへアクセスすると「Cannot read properties of null」というエラーになります。
list.addEventListener("click", (event) => {
const button = event.target.closest(".done");
// NG: button が null のときにエラーになる
// button.closest("li").classList.add("completed");
// OK: null を先に弾いてから処理する
if (!button) return;
button.closest("li").classList.add("completed");
});
closest は「見つからないこともある」メソッドなので、戻り値を使う前に必ず null かどうかを確認する習慣をつけると、この種のエラーを防げます。
使い分けの整理
最後に3つのメソッドを整理します。matches はその要素自身が条件に合うかを真偽値で調べたいとき。closest は一番近い祖先(自分を含む)を取得したいときで、イベント委譲での要素判定に最適です。contains はある要素が自分の内側にあるかを確認したいとき、たとえば「外側クリックの検知」で役立ちます。いずれも CSS セレクタや DOM ノードひとつで判定でき、parentNode を while でたどるような手書きの探索を書かずに済みます。用途に合わせて選べば、DOM の判定コードは短く読みやすくなります。