- JavaScript
JS querySelectorの使い方|All・nullの対処
querySelectorとは、CSSセレクタに一致する最初の要素を1つ返すメソッドです。 一致する要素がなければ null を返します。すべての要素がほしいときは querySelectorAll を使い、こちらは一致した要素をまとめた NodeList(要素の一覧)を返します。
この記事では、セレクタの書き方24例を「実際に何が取れたか」つきの早見表にまとめました。あわせて、querySelectorAll と getElementsByClassName の戻り値の違いを要素追加後の件数で比べ、null が返る原因をエラー文の再現と対処コードで整理しています。掲載した出力はすべて、Chromium 141(Playwright 1.56.0 から起動)で実際に動かして得たものです。
querySelectorの早見表
まずは全体像です。「1つだけほしいなら querySelector、全部ほしいなら querySelectorAll」 が基本です。
| やりたいこと | 書き方 | 戻り値 | 見つからないとき |
|---|---|---|---|
| 最初の1つを取る | document.querySelector('.item') |
Element | null |
| 全部取る | document.querySelectorAll('.item') |
NodeList(静的) | 空の NodeList(length が 0) |
| id で1つ取る | document.getElementById('top') |
Element | null |
| class で全部取る | document.getElementsByClassName('item') |
HTMLCollection(ライブ) | 空の HTMLCollection |
| ある要素の中だけ探す | ul.querySelector('.sold') |
Element | null |
| 親方向に探す | li.closest('ul') |
Element | null |
| セレクタに合うか調べる | li.matches('.item.sold') |
true / false | false |
| 全部に同じ処理をする | document.querySelectorAll('.item').forEach(...) |
なし | 0回実行(エラーなし) |
| 配列メソッドを使う | [...document.querySelectorAll('.item')].map(...) |
配列 | 空の配列 |
| null でも止めずに読む | document.querySelector('.x')?.textContent |
値 または undefined | undefined |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JS querySelectorの使い方|All・nullの対処」
この記事で使う練習用HTML
以降の取得結果は、すべて次のHTMLを対象にしています。そのまま index.html として保存し、ブラウザの開発者ツールのコンソールで試せます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>querySelector 練習ページ</title>
</head>
<body>
<header id="top">
<h1 class="title">備品リスト</h1>
<nav>
<a href="/home" class="link">ホーム</a>
<a href="https://example.com/help" class="link external" target="_blank">ヘルプ</a>
<a href="/files/list.pdf" class="link">一覧PDF</a>
</nav>
</header>
<main>
<ul id="items" class="list">
<li class="item" data-id="1" data-status="in-stock">ノート</li>
<li class="item sold" data-id="2" data-status="sold-out">ペン</li>
<li class="item" data-id="3" data-status="in-stock">付箋</li>
<li class="item special" data-id="4">ホチキス</li>
<li class="item" data-id="5" data-status="in-stock">はさみ</li>
</ul>
<form id="form">
<input type="text" name="keyword" placeholder="検索語">
<input type="checkbox" name="agree" checked>
<input type="email" name="mail" disabled>
<button type="submit" class="btn primary">検索</button>
</form>
<p class="note">在庫は毎朝更新します。</p>
<div id="1st">数字で始まるid</div>
<div id="user.name">ドットを含むid</div>
</main>
</body>
</html>
querySelectorとquerySelectorAllの基本
1つ取る:querySelector
引数にはCSSセレクタを文字列で渡します。CSSで書くときと同じ記法(id は #、class は .)がそのまま使えます。
const title = document.querySelector('.title');
console.log(title.textContent); // 備品リスト
const first = document.querySelector('.item');
console.log(first.textContent); // ノート(5つあっても最初の1つ)
const none = document.querySelector('.nothing');
console.log(none); // null
一致する要素が複数あっても、返るのは文書の上から数えて最初の1つだけです。
全部取る:querySelectorAll
const items = document.querySelectorAll('.item');
console.log(items.length); // 5
console.log(items[0].textContent); // ノート
items.forEach((el, i) => {
console.log(i, el.textContent);
});
実行結果:
0 ノート
1 ペン
2 付箋
3 ホチキス
4 はさみ
querySelectorAll は、一致が0件でも null にはならず、空の NodeList を返します。 そのため forEach をそのまま呼んでもエラーになりません(0回実行されるだけです)。
2つの違いを表で確認
| 比べる点 | querySelector | querySelectorAll |
|---|---|---|
| 返すもの | 最初の1要素 | 一致した全要素の NodeList |
| 一致なしのとき | null | 空の NodeList(length は 0) |
| そのまま textContent を読む | 読める | undefined(一覧なので要素ではない) |
| そのまま addEventListener | 使える | TypeError(1つずつ登録する) |
| forEach | 使えない(要素1つ) | 使える |
| 向いている場面 | id 指定・1つしかない要素 | リスト・複数のボタンやリンク |
セレクタの書き方早見表(24例と取得結果)
上の練習用HTMLに対して、それぞれのセレクタを querySelector と querySelectorAll に渡した結果です。「件数」と「中身」は実際の出力をそのまま載せています。
id・class・タグ
| やりたいこと | セレクタ | querySelector の結果 | All の件数 | All の中身 |
|---|---|---|---|---|
| id で取る | #top |
header 要素 | 1 | header |
| class で取る | .title |
備品リスト | 1 | 備品リスト |
| タグ名で取る | li |
ノート | 5 | ノート / ペン / 付箋 / ホチキス / はさみ |
| class を2つとも持つ | .item.sold |
ペン | 1 | ペン |
| 複数のどれか(カンマ) | h1, p |
備品リスト | 2 | 備品リスト / 在庫は毎朝更新します。 |
| 一致なし | .nothing |
null | 0 | (空) |
親子・兄弟の関係
| やりたいこと | セレクタ | querySelector の結果 | All の件数 | All の中身 |
|---|---|---|---|---|
| 直下の子 | ul > li |
ノート | 5 | ノート / ペン / 付箋 / ホチキス / はさみ |
| 子孫(スペース区切り) | nav a |
ホーム | 3 | ホーム / ヘルプ / 一覧PDF |
| 直後の兄弟 | h1 + nav |
nav 要素 | 1 | nav |
| 後ろにある兄弟すべて | .sold ~ li |
付箋 | 3 | 付箋 / ホチキス / はさみ |
| 絞り込みを重ねる | #form button.primary |
検索 | 1 | 検索 |
属性
| やりたいこと | セレクタ | querySelector の結果 | All の件数 | All の中身 |
|---|---|---|---|---|
| 属性を持つ | [data-status] |
ノート | 4 | ノート / ペン / 付箋 / はさみ |
| 属性値が一致 | [data-status="sold-out"] |
ペン | 1 | ペン |
| 値が〜で始まる | [href^="https"] |
ヘルプ | 1 | ヘルプ |
| 値が〜で終わる | [href$=".pdf"] |
一覧PDF | 1 | 一覧PDF |
| 値に〜を含む | [href*="example"] |
ヘルプ | 1 | ヘルプ |
順番・否定・状態(擬似クラス)
| やりたいこと | セレクタ | querySelector の結果 | All の件数 | All の中身 |
|---|---|---|---|---|
| 最初の子 | li:first-child |
ノート | 1 | ノート |
| 最後の子 | li:last-child |
はさみ | 1 | はさみ |
| n番目の子 | li:nth-child(2) |
ペン | 1 | ペン |
| 奇数番目 | li:nth-child(odd) |
ノート | 3 | ノート / 付箋 / はさみ |
| 〜ではない | li:not(.sold) |
ノート | 4 | ノート / 付箋 / ホチキス / はさみ |
| 属性を持たない | li:not([data-status]) |
ホチキス | 1 | ホチキス |
| チェック済み | input[type="checkbox"]:checked |
name が agree の input | 1 | agree |
| 無効化されている | input:disabled |
name が mail の input | 1 |
nth-child は1から数えます(配列の添字のように0からではありません)。odd は奇数番目、even は偶数番目、3n と書くと3の倍数番目です。
早見表を自分のページで試すコード
次のコードをコンソールに貼ると、同じ形式で結果を確認できます。
const selectors = ['#top', '.item.sold', '[data-status]', 'li:not(.sold)'];
for (const s of selectors) {
const one = document.querySelector(s);
const all = document.querySelectorAll(s);
console.log(
s,
'| 1つ目:', one === null ? 'null' : one.textContent.trim().slice(0, 12),
'| 件数:', all.length
);
}
getElementByIdとの違いと使い分け
getElementById は id 専用、querySelector はCSSセレクタ全般を受け取ります。引数の書き方が違う点(# を付けるかどうか)が、取り違えやすいポイントです。
console.log(document.getElementById('top')); // header 要素
console.log(document.getElementById('#top')); // null(# は不要)
console.log(document.querySelector('#top')); // header 要素
console.log(document.querySelector('top')); // null(top というタグを探してしまう)
console.log(document.querySelector('#top') === document.getElementById('top')); // true
最後の行が true になるとおり、同じ要素を指していれば、どちらで取っても同じオブジェクトです。
| 比べる点 | getElementById | querySelector |
|---|---|---|
| 引数 | id の文字列そのまま('top') |
CSSセレクタ('#top') |
| 呼び出せる場所 | document | document と各要素 |
| 条件の組み合わせ | できない | できる('#form button.primary' など) |
| 見つからないとき | null | null |
| 数字で始まる id | そのまま取れる | エスケープが必要(次の節) |
| 向いている場面 | id が決まっている1要素 | class・属性・階層を組み合わせたい |
id が決まっていて記号を含まないなら、どちらを使っても結果は同じです。チーム内でどちらかに統一しておくと読みやすくなります。条件を組み合わせる必要があるときは querySelector を選びます。
数字で始まるid・ドットを含むid
練習用HTMLには、id が 1st の要素と user.name の要素があります。これを querySelector で取ろうとすると、次の結果になりました。
document.getElementById('1st'); // div 要素(取れる)
document.querySelector('#1st'); // SyntaxError
document.querySelector('#user.name'); // null
実行結果(Chromium 141):
SyntaxError: Failed to execute 'querySelector' on 'Document': '#1st' is not a valid selector.
#1st はCSSの識別子として書けない形のため、null ではなく SyntaxError になります。#user.name は「id が user で、class に name を持つ要素」と解釈されるので、該当なしの null です。
対処は次の3通りです。変数で受け取った id を使うときは CSS.escape が便利です。
// 1. CSS.escape でエスケープする
document.querySelector('#' + CSS.escape('1st')); // 取れる
console.log(CSS.escape('1st')); // \31 st
// 2. 属性セレクタで書く
document.querySelector('[id="user.name"]'); // 取れる
// 3. バックスラッシュでエスケープする(JS文字列では2つ重ねる)
document.querySelector('#user\\.name'); // 取れる
NodeListとHTMLCollectionの違い
querySelectorAll が返すのは 静的な NodeList、getElementsByClassName が返すのは ライブな HTMLCollection です。「静的」は取得した時点の内容で固定されること、「ライブ」はページの変化に合わせて中身が自動で変わることを指します。
要素を追加したあとの件数を比べる
const nodeList = document.querySelectorAll('.item'); // 静的
const collection = document.getElementsByClassName('item'); // ライブ
const childNodes = document.getElementById('items').childNodes; // ライブな NodeList
console.log(`追加前: NodeList=${nodeList.length} HTMLCollection=${collection.length} childNodes=${childNodes.length}`);
const li = document.createElement('li');
li.className = 'item';
li.textContent = 'クリップ';
document.getElementById('items').appendChild(li);
console.log(`追加後: NodeList=${nodeList.length} HTMLCollection=${collection.length} childNodes=${childNodes.length}`);
console.log(`取り直し: ${document.querySelectorAll('.item').length}`);
実行結果:
追加前: NodeList=5 HTMLCollection=5 childNodes=11
追加後: NodeList=5 HTMLCollection=6 childNodes=12
取り直し: 6
querySelectorAll の結果は5件のまま、getElementsByClassName の結果は6件に増えました。 追加した要素も含めたいときは、querySelectorAll をもう一度呼んで取り直します。
childNodes が11件なのは、li 要素5つに加えて、改行やインデントのテキストノードが6つ含まれるためです。また、childNodes は同じ NodeList でもライブです。「NodeList だから静的」ではなく「querySelectorAll が返す NodeList が静的」 と覚えると正確です。
forEachが使える・使えない
console.log(typeof nodeList.forEach); // function
console.log(typeof collection.forEach); // undefined
console.log(Array.isArray(nodeList)); // false
collection.forEach((el) => console.log(el));
nodeList.map((el) => el.textContent);
実行結果:
TypeError: collection.forEach is not a function
TypeError: nodeList.map is not a function
NodeList は forEach を持ちますが配列ではないので、map・filter・reduce などは持っていません。HTMLCollection は forEach も持ちません。整理すると次のとおりです。
| 比べる点 | NodeList(querySelectorAll) | HTMLCollection(getElementsBy〜) | 配列 |
|---|---|---|---|
| 取得後にページが変わると | 変わらない(静的) | 自動で変わる(ライブ) | 変わらない |
| length・添字アクセス | 使える | 使える | 使える |
| forEach | 使える | 使えない | 使える |
| for...of | 使える | 使える | 使える |
| entries() / keys() / values() | 使える | 使えない | 使える |
| map / filter / reduce | 使えない | 使えない | 使える |
| Array.isArray | false | false | true |
配列に変換して map・filter を使う
スプレッド構文か Array.from で配列に変換すれば、すべての配列メソッドが使えます。
// スプレッド構文
const ids = [...document.querySelectorAll('.item')].map((el) => el.dataset.id);
console.log(ids.join(',')); // 1,2,3,4,5
// Array.from(HTMLCollection にも使える)
const names = Array.from(document.getElementsByClassName('item')).map((el) => el.textContent);
console.log(names.join(',')); // ノート,ペン,付箋,ホチキス,はさみ,クリップ
// 添字つきで回したいとき
for (const [i, el] of document.querySelectorAll('.item').entries()) {
console.log(i, el.textContent);
}
2つ目の結果に「クリップ」が入っているのは、先ほど要素を追加したあとに実行したためです。
ライブなコレクションをループで書き換えるとどうなるか
ライブなコレクションは、ループの途中で中身が変わります。全要素から class を外すつもりで書いたコードが、一部だけ残る例を見てみます。
// ライブな HTMLCollection を for で回して class を外す
const live = document.getElementsByClassName('item');
for (let i = 0; i < live.length; i++) {
live[i].classList.remove('item');
}
console.log(document.querySelectorAll('.item').length);
実行結果(要素が6つある状態で実行):
3
残ったのは「ペン・ホチキス・クリップ」の3つでした。class を外した瞬間にその要素がコレクションから抜け、後ろの要素が前に詰まるため、1つおきに処理が飛びます。
静的な NodeList なら、同じ処理が全要素に行き渡ります。
document.querySelectorAll('.item').forEach((el) => el.classList.remove('item'));
console.log(document.querySelectorAll('.item').length); // 0
| 選び方 | 向いている取得方法 |
|---|---|
| 取得した時点の要素に、まとめて処理をしたい | querySelectorAll |
| ループ中に class の付け外しや要素の削除をする | querySelectorAll |
| 常に最新の件数を参照したい | getElementsByClassName、または都度 querySelectorAll |
| map・filter で加工したい | querySelectorAll を配列に変換 |
複数の要素にまとめて処理する書き方
querySelectorAll の戻り値は一覧なので、要素のプロパティやメソッドを直接呼ぶことはできません。 よくある3つの書き方と、その結果です。
document.querySelectorAll('.item').textContent;
document.querySelectorAll('.item').addEventListener('click', () => {});
document.querySelectorAll('.item').classList.add('x');
実行結果:
undefined
TypeError: document.querySelectorAll(...).addEventListener is not a function
TypeError: Cannot read properties of undefined (reading 'add')
1つ目はエラーにならず undefined が返るので、気づきにくい点です。forEach で1つずつ処理する形に直します。
// 直したあと:1つずつ処理する
document.querySelectorAll('.item').forEach((el) => {
el.classList.add('x');
el.addEventListener('click', () => {
console.log(el.textContent);
});
});
ある要素の中だけを探す
querySelector と querySelectorAll は、document だけでなく各要素からも呼べます。探す範囲をその要素の子孫に絞れるので、同じ class がページ内の別の場所にあっても取り違えません。
const ul = document.querySelector('#items');
console.log(ul.querySelector('.sold').textContent); // ペン
console.log(ul.querySelector('h1')); // null(ul の外にある)
注意したいのは、セレクタの照合そのものは文書全体に対して行われる点です。次の例では、ul の外側にある main を含むセレクタでも一致しました。
console.log(ul.querySelector('main li').textContent); // ノート
「ul 自身を起点にした直下の子」のように書きたいときは、:scope を使います。:scope は呼び出し元の要素そのものを指します。
console.log(ul.querySelector(':scope > li').textContent); // ノート
console.log(ul.querySelectorAll(':scope > .item').length); // 6(クリップを追加したあとのため)
親方向に探す closest と、判定する matches
querySelector は子孫の方向にしか探しません。親の方向に探すときは closest を使います。
const li = document.querySelector('.sold');
console.log(li.closest('ul').id); // items
console.log(li.matches('.item.sold')); // true
closest は、あとで紹介するイベント委任でもよく使います。
nullが返る原因と対処
querySelector の結果が null のままプロパティを読むと、次のエラーで処理が止まります。
document.querySelector('.nothing').textContent;
TypeError: Cannot read properties of null (reading 'textContent')
このエラーが出たら、「読もうとしたプロパティ」ではなく「その手前の querySelector が null を返した」ことが原因です。 null になる主な原因は次の3つです。
| 原因 | 確かめ方 | 対処 |
|---|---|---|
| script が要素より先に実行されている | script タグの位置を見る | defer を付ける / body の末尾に置く / DOMContentLoaded を待つ |
| セレクタの書き間違い | コンソールで同じセレクタを実行する | # と . の有無、大文字小文字、記号のエスケープを見直す |
| あとから追加される要素 | 追加の前後で実行してみる | 追加後に取得する / イベント委任 / MutationObserver |
原因1:scriptの位置(要素より先に実行されている)
ブラウザはHTMLを上から順に読みます。head の中に書いたスクリプトは、body の中の要素がまだ作られていない時点で実行されます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>n1</title>
<script>
const btn = document.querySelector('#save');
console.log('head内:', btn);
btn.addEventListener('click', () => console.log('保存'));
</script>
</head>
<body>
<button id="save">保存</button>
</body>
</html>
実行結果:
head内: null
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
対処は3通りあり、どれも「要素ができてから実行する」ようにするものです。
対処A:外部ファイルにして defer を付ける
<head>
<script src="app.js" defer></script>
</head>
<body>
<button id="save">保存</button>
</body>
// app.js
console.log('defer:', document.querySelector('#save').outerHTML, '/ readyState =', document.readyState);
対処B:DOMContentLoaded を待つ、または body の末尾に書く
<head>
<script>
document.addEventListener('DOMContentLoaded', () => {
const btn = document.querySelector('#save');
console.log('DOMContentLoaded後:', btn.outerHTML);
});
</script>
</head>
<body>
<button id="save">保存</button>
<script>
console.log('body末尾:', document.querySelector('#save').outerHTML);
</script>
</body>
実行結果(対処AとBをそれぞれ実行):
defer: <button id="save">保存</button> / readyState = interactive
body末尾: <button id="save">保存</button>
DOMContentLoaded後: <button id="save">保存</button>
type="module" を付けたスクリプトも、defer と同じくHTMLの解析が終わってから実行されます。head の中に module のスクリプトを書いて同じ取得を試したところ、次のとおり要素が取れました。
module: <button id="save">保存</button> / readyState = interactive
| 書き方 | 実行されるタイミング | 向いている場面 |
|---|---|---|
| head に script(属性なし) | その場で即実行(body はまだ無い) | DOMに触れない初期設定 |
| script に defer | HTMLの解析が終わったあと | 外部ファイルの基本形 |
| script に type="module" | HTMLの解析が終わったあと | import を使う構成 |
| body の末尾に script | そこまでの要素が作られたあと | 小さなインラインスクリプト |
| DOMContentLoaded の中 | HTMLの解析が終わったあと | script の位置を変えられないとき |
原因2:セレクタの書き間違い
練習用HTMLに対して、よくある書き間違いを実行した結果です。ほとんどはエラーにならず、静かに null が返ります。
| 書いたコード | 結果 | 理由 | 正しい書き方 |
|---|---|---|---|
querySelector('top') |
null | # が無く、top タグを探している | '#top' |
querySelector('item') |
null | . が無く、item タグを探している | '.item' |
querySelector('#Top') |
null | id は大文字と小文字を区別する | '#top' |
querySelector('.Title') |
null | class も大文字と小文字を区別する | '.title' |
getElementById('#top') |
null | getElementById に # は不要 | 'top' |
querySelector('#user.name') |
null | . 以降が class と解釈される | '[id="user.name"]' |
querySelector('#1st') |
SyntaxError | 数字で始まる id はそのまま書けない | '#' + CSS.escape('1st') |
querySelector('') |
SyntaxError | 空文字は渡せない | セレクタを渡す |
空文字を渡したときのエラー全文は次のとおりです。変数にセレクタを入れて渡す場合、変数が空になっているとこのエラーになります。
SyntaxError: Failed to execute 'querySelector' on 'Document': The provided selector is empty.
切り分けの手順は、開発者ツールのコンソールで同じセレクタを直接実行することです。 コンソールで取れるのにスクリプトでは null になるなら原因1か原因3、コンソールでも null ならセレクタの書き方が原因です。
原因3:あとから追加した要素
JavaScript で追加する要素は、追加する前には存在しません。次のHTMLで、読み込み時とボタンを押したあとの違いを確かめます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>n4</title>
</head>
<body>
<ul id="list"><li class="row">A</li></ul>
<button id="add">追加</button>
<script>
const list = document.querySelector('#list');
// (1) 読み込み時に取得して個別に登録
document.querySelectorAll('.row').forEach((li) => {
li.addEventListener('click', () => console.log('個別登録:', li.textContent));
});
// (2) 親に1つだけ登録(イベント委任)
list.addEventListener('click', (e) => {
const li = e.target.closest('.row');
if (li) console.log('委任:', li.textContent);
});
console.log('追加前に .new を探す:', document.querySelector('.new'));
document.querySelector('#add').addEventListener('click', () => {
const li = document.createElement('li');
li.className = 'row new';
li.textContent = 'B';
list.appendChild(li);
console.log('追加後に .new を探す:', document.querySelector('.new').textContent);
});
</script>
</body>
</html>
「A をクリック → 追加ボタンをクリック → B をクリック」の順に操作した実行結果:
追加前に .new を探す: null
個別登録: A
委任: A
追加後に .new を探す: B
委任: B
読み取れることは2つです。
- 追加する前は null、追加したあとは取れる。 取得するタイミングを、追加の処理のあとに移せば解決します。
- 読み込み時に forEach で登録したクリック処理は、あとから追加した B には付いていない(「個別登録: B」が出ていない)。親要素に1つだけ登録するイベント委任なら、B のクリックも拾えています。
イベント委任は、親要素でイベントを受け取り、e.target.closest('.row') で「クリックされた場所から見て一番近い対象要素」を探す書き方です。増減するリストには、この形が扱いやすくなります。
追加されるタイミングがわからないとき:MutationObserver
通信の結果を待って描画する部品など、いつ要素が追加されるか自分のコードからわからない場合は、MutationObserver(DOMの変化を監視する仕組み)で追加を待てます。
<div id="app"></div>
<script>
const app = document.querySelector('#app');
const observer = new MutationObserver(() => {
const el = document.querySelector('.late');
if (el) {
console.log('MutationObserverで検出:', el.textContent);
observer.disconnect(); // 見つけたら監視を終える
}
});
observer.observe(app, { childList: true, subtree: true });
console.log('直後:', document.querySelector('.late'));
// 0.3秒後に要素が追加される想定
setTimeout(() => {
app.innerHTML = '<p class="late">あとから表示</p>';
}, 300);
</script>
実行結果:
直後: null
MutationObserverで検出: あとから表示
nullでも止まらない書き方
要素が無いこともありうる場合は、null を前提にした書き方にしておくと、ほかの処理まで止まることを防げます。
// 1. if で確かめる(基本形)
const note = document.querySelector('.note');
if (note) {
note.textContent = '更新しました';
}
// 2. オプショナルチェーン(?.)で読む
const text = document.querySelector('.nothing')?.textContent;
console.log(text); // undefined(エラーにならない)
// 3. 既定値を付ける(??)
const label = document.querySelector('.nothing')?.textContent ?? '(未設定)';
// 4. メソッド呼び出しにも使える
document.querySelector('.nothing')?.addEventListener('click', () => {});
// 5. 必ずあるはずの要素は、見つからないことを早めに知らせる
function mustQuery(selector) {
const el = document.querySelector(selector);
if (el === null) {
throw new Error(`要素が見つかりません: ${selector}`);
}
return el;
}
| 状況 | 向いている書き方 |
|---|---|
| ページによって要素があったり無かったりする | if または ?. |
| 無いときの代わりの値がある | ?. と ?? の組み合わせ |
| 必ずあるはずで、無いなら設定の誤り | 専用の関数で、セレクタ名つきのエラーを投げる |
| 複数の要素に処理する | querySelectorAll と forEach(0件でもエラーにならない) |
オプショナルチェーンは代入の左辺には使えません(el?.textContent = 'x' は構文エラー)。値を書き換えるときは if で確かめる形にします。
よく使う取得パターン集
そのまま貼って使える形でまとめます。
// フォームの入力値を取る
const keyword = document.querySelector('#form input[name="keyword"]').value;
// チェックされたチェックボックスの name を集める
const checked = [...document.querySelectorAll('input[type="checkbox"]:checked')]
.map((el) => el.name);
// data 属性で絞り込んで1つ取る
const id = 3;
const target = document.querySelector(`.item[data-id="${id}"]`);
// 外部リンクすべてに rel を付ける
document.querySelectorAll('a[href^="https"]').forEach((a) => {
a.rel = 'noopener';
});
// 在庫ありの件数を数える
const count = document.querySelectorAll('.item[data-status="in-stock"]').length;
// 売り切れ以外に class を付ける
document.querySelectorAll('.item:not(.sold)').forEach((el) => {
el.classList.add('available');
});
変数をセレクタに埋め込むときは、値に記号が入る可能性があるなら CSS.escape を通すと安心です。
const userId = 'user.name';
const el = document.querySelector('#' + CSS.escape(userId));
つまずいたときのチェックリスト
| 確認すること | 見るポイント |
|---|---|
| エラー文に null とあるか | 手前の querySelector が何も見つけていない |
| コンソールで同じセレクタを実行したか | コンソールで取れるなら実行タイミングが原因 |
| # と . を付けたか | getElementById は付けない、querySelector は付ける |
| 大文字と小文字は合っているか | id・class は区別される |
| script はどこにあるか | head にあるなら defer か DOMContentLoaded |
| その要素はあとから追加されるものか | 追加後に取得する、または親に委任する |
| querySelectorAll の戻り値に直接処理していないか | forEach で1つずつ処理する |
| is not a function と出たか | HTMLCollection に forEach、NodeList に map を呼んでいないか |
よくある質問
Q. querySelector と getElementById はどちらを使えばよいですか? A. id で1つ取るだけなら結果は同じなので、どちらでも構いません。class や属性、階層を組み合わせたいときは querySelector を選びます。数字で始まる id や記号を含む id は、getElementById ならそのまま取れ、querySelector では CSS.escape か属性セレクタが必要です。
Q. querySelectorAll の結果に map が使えないのはなぜですか?
A. 戻り値の NodeList は配列ではないためです。forEach・entries・for...of は使えますが、map や filter は持っていません。[...document.querySelectorAll('.item')] または Array.from で配列に変換すると使えるようになります。
Q. 要素は画面に表示されているのに null になります。なぜですか? A. スクリプトが実行された時点では、まだその要素が作られていなかった可能性があります。script が head にあるなら defer を付けるか DOMContentLoaded を待ちます。JavaScript であとから追加される要素なら、追加後に取得するか、親要素へのイベント委任に切り替えます。
Q. querySelectorAll で取ったあとに追加した要素も、一覧に入りますか? A. 入りません。querySelectorAll の NodeList は取得した時点で固定されます(今回の検証では、追加後も5件のままでした)。追加後の要素も含めたいときは、もう一度 querySelectorAll を呼びます。
まとめ
- querySelector は最初の1つ、querySelectorAll は全部を返す。見つからないとき、前者は null、後者は空の NodeList
- セレクタはCSSと同じ記法。id は #、class は .、属性は角かっこ、否定は :not、順番は :nth-child(1から数える)
- querySelectorAll の NodeList は静的で、要素を追加しても件数は変わらない。getElementsByClassName の HTMLCollection はライブで自動的に増減する
- forEach は NodeList で使え、HTMLCollection では使えない。map・filter を使うときはスプレッド構文か Array.from で配列にする
- null の原因は「script の位置」「セレクタの書き間違い」「あとから追加した要素」 の3つに分けて確かめる
- null でも止まらない書き方は、if・オプショナルチェーン・イベント委任を場面に合わせて選ぶ
- 数字で始まる id や記号を含む id は CSS.escape か属性セレクタで取る
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JS querySelectorの使い方|All・nullの対処」
株式会社RJCでは、querySelectorをはじめとするDOM操作の基本を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。