- JavaScript
JavaScript DOM操作一覧|querySelector〜イベント・コピペ可
JavaScriptのDOM操作とは、HTMLをツリー構造のオブジェクト(DOM)として扱い、要素の取得・追加・削除・書き換え・イベント登録をコードから行うことです。 基本は「querySelector で取る → textContent や classList で変える → addEventListener で反応する」の3ステップです。
この記事では、DOM操作の全メソッドを12系統の一覧表にまとめ、戻り値・NodeListかHTMLCollectionか・ライブか静的かまで確認できるようにしました。closest・replaceChildren・before/after・toggle(force)・AbortController など新しめのAPIも含め、すべてのコードをPlaywright 1.56+Chromium 141(ヘッドレス)で実行し、結果をコメントで載せています。イベント委譲、DocumentFragmentの効果の計測、よくあるエラー文と直し方まで、この1本で確認できます。
JavaScript DOM操作の早見表(12系統)
| 系統 | やりたいこと | 書き方 | 戻り値 |
|---|---|---|---|
| 取得 | CSSセレクタで1つ | document.querySelector(".item") |
要素 / null |
| 取得 | CSSセレクタで全部 | document.querySelectorAll(".item") |
NodeList(静的) |
| 取得 | idで1つ | document.getElementById("list") |
要素 / null |
| 取得 | クラス名・タグ名で | getElementsByClassName / getElementsByTagName |
HTMLCollection(ライブ) |
| 取得 | 祖先を探す | el.closest("ul") |
要素 / null(自分も対象) |
| 取得 | 条件に合うか | el.matches(".done") |
true / false |
| 作成 | 要素を作る | document.createElement("li") |
新しい要素 |
| 作成 | 複製する | el.cloneNode(true) |
複製(trueで子孫も) |
| 作成 | ひな形から | tpl.content.cloneNode(true) |
DocumentFragment |
| 追加 | 末尾・先頭(複数・文字列可) | parent.append(a, "文字") / prepend |
undefined |
| 追加 | 直前・直後 | el.before(a) / el.after(a) |
undefined |
| 追加 | 末尾に1つ(従来) | parent.appendChild(a) |
追加したノード |
| 追加 | HTML文字列を位置指定 | el.insertAdjacentHTML("beforeend", html) |
undefined |
| 削除 | 自分を消す | el.remove() |
undefined |
| 削除 | 子を消す(従来) | parent.removeChild(child) |
消したノード |
| 削除 | 子を全部消す | parent.replaceChildren() |
undefined |
| 置換 | 自分を置き換える | el.replaceWith(newEl) |
undefined |
| 置換 | 子を全部入れ替える | parent.replaceChildren(a, b) |
undefined |
| 属性 | 読む・書く・有無・削除 | getAttribute / setAttribute / hasAttribute / removeAttribute |
文字列・null / undefined / 真偽値 |
| 属性 | 付け外し | el.toggleAttribute("hidden", 条件) |
付いた後の状態 |
| 属性 | data-属性 | el.dataset.userName |
文字列 / undefined |
| クラス | 追加・削除・判定 | classList.add / remove / contains / replace |
undefined / 真偽値 |
| クラス | 切り替え | el.classList.toggle("on", 条件) |
付いた後の状態 |
| スタイル | インライン・CSS変数 | el.style.color = "red" / style.setProperty("--gap","8px") |
― |
| スタイル | 最終的な値を読む | getComputedStyle(el).fontSize |
文字列("20px") |
| テキスト/HTML | 文字を読む・書く | el.textContent |
非表示も含む文字列 |
| テキスト/HTML | 見えている文字 | el.innerText |
非表示を除く文字列 |
| テキスト/HTML | HTMLとして | el.innerHTML / outerHTML |
HTML文字列 |
| 走査 | 親・子 | parentElement / children / childNodes |
要素 / HTMLCollection / NodeList |
| 走査 | 最初・最後の子、兄弟 | firstElementChild / nextElementSibling など |
要素 / null |
| イベント | 登録・解除 | addEventListener(型, fn, opts) / removeEventListener |
undefined |
| イベント | まとめて解除・1回だけ | { signal: ac.signal } / { once: true } |
― |
| イベント | 発火させる | el.click() / el.dispatchEvent(new CustomEvent(...)) |
undefined / 真偽値 |
| フォーム | 値・チェック | form.elements.title.value / cb.checked |
文字列 / 真偽値 |
| フォーム | まとめて読む | Object.fromEntries(new FormData(form)) |
オブジェクト |
| フォーム | 送信を止める・送る | e.preventDefault() / form.requestSubmit() |
― |
迷ったら「取得は querySelector、追加は append、削除は remove、イベントは addEventListener+委譲」 を基本にすると、統一された読みやすいコードになります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScript DOM操作一覧|querySelector〜イベント・コピペ可」
検証環境と共通のHTML
以降のコードは下のHTMLを読み込んだページで1つずつ実行し、戻り値を // → のコメントで示しています。 環境は Playwright 1.56.0 + Chromium 141.0.7390.37(ヘッドレス)、Node.js 22.22.2 です。
<ul id="list" class="items">
<li class="item" data-id="1">りんご</li>
<li class="item" data-id="2">みかん</li>
<li class="item done" data-id="3">ぶどう</li>
</ul>
<p id="msg">こんにちは<span style="display:none">(非表示)</span></p>
<form id="f">
<input name="title" value="買い物">
<select name="pri"><option value="low">低</option><option value="high" selected>高</option></select>
<input type="checkbox" name="done" checked>
<button>送信</button>
</form>
<template id="tpl"><li class="item"><span class="name"></span></li></template>
<script src="main.js" defer></script>
<script> には defer を付けます。 <head> の中でそのまま実行すると、まだ読み込まれていない要素は取得できません(「よくあるエラー」で結果を示します)。
要素を取得する(querySelector・closest)
document.querySelector(".item").textContent; // → "りんご"
const all = document.querySelectorAll(".item");
[all.constructor.name, all.length, all[2].textContent]; // → ["NodeList", 3, "ぶどう"]
all.forEach((li) => console.log(li.dataset.id)); // → 1 / 2 / 3
document.querySelector(".nope"); // → null
document.querySelectorAll(".nope").length; // → 0
querySelector は最初に一致した1つ、querySelectorAll は一致したすべてを返します。 セレクタはCSSと同じ(#id、.class、[data-id="2"]、ul > li:first-child)で、見つからないときは1つ取る系が null、全部取る系が空のリストです。
取得メソッドごとの戻り値の型(実行結果)
| 書き方 | 戻り値の型 | ライブ/静的 | forEach |
|---|---|---|---|
querySelectorAll |
NodeList | 静的 | あり |
getElementsByClassName / getElementsByTagName |
HTMLCollection | ライブ | なし |
getElementsByName |
NodeList | ライブ | あり |
el.children |
HTMLCollection | ライブ | なし |
el.childNodes |
NodeList | ライブ | あり |
form.elements |
HTMLFormControlsCollection | ライブ | なし |
getElementById |
要素そのもの(例:HTMLUListElement) | ― | ― |
どれも配列ではありません。 map や filter を使うときは [...list] か Array.from(list) で配列にします。
ライブと静的の違い、children と childNodes の違い
const ul = document.querySelector("#list");
const staticList = document.querySelectorAll(".item");
const liveList = document.getElementsByClassName("item");
[ul.children.length, ul.childNodes.length]; // → [3, 7]
[ul.firstChild.nodeName, ul.firstElementChild.nodeName]; // → ["#text", "LI"]
const li = document.createElement("li");
li.className = "item";
ul.append(li);
[staticList.length, liveList.length, ul.childNodes.length]; // → [3, 4, 8]
ライブなリストはDOMの変化に合わせて自動で増減し、静的なリストは取得した瞬間のままです。 childNodes は改行や空白のテキストノードも含むため7、children は要素だけなので3です。要素だけを扱うなら children・firstElementChild のように Element が付くほうを使います。
祖先を探す closest と matches
const li = document.querySelector('[data-id="2"]');
li.closest("ul").id; // → "list"
li.closest(".nope"); // → null
li.closest("li") === li; // → true(自分自身も判定に含む)
li.matches(".done"); // → false
li.parentElement.contains(li); // → true
closest は自分自身から親方向へたどり、最初に一致した要素を返します。 後述のイベント委譲で中心になるメソッドです。
要素を作成・追加する(append・before/after・insertAdjacentHTML)
const ul = document.getElementById("list");
const two = ul.children[1]; // みかん
const mk = (t) => { const e = document.createElement("li"); e.textContent = t; return e; };
two.before(mk("前"));
two.after(mk("後"));
ul.prepend(mk("先頭"));
ul.append(mk("末尾"));
[...ul.children].map((e) => e.textContent).join(",");
// → "先頭,りんご,前,みかん,後,ぶどう,末尾"
prepend/append は「親の中」、before/after は「自分の外側」に追加します。 従来の insertBefore(新, 基準) は 基準.before(新) で書けます。
const ul = document.getElementById("list");
ul.replaceChildren();
const a = document.createElement("li");
a.textContent = "A";
ul.append(a, "テキスト", document.createElement("hr")); // → undefined
ul.innerHTML; // → "<li>A</li>テキスト<hr>"
const b = document.createElement("li");
ul.appendChild(b) === b; // → true(追加したノードを返す)
append |
appendChild |
|
|---|---|---|
| 一度に追加できる数 | 複数 | 1つ |
| 文字列を渡す | テキストノードとして追加 | エラー(後述) |
| 戻り値 | undefined |
追加したノード |
insertAdjacentHTML の4つの位置
const two = document.querySelector('[data-id="2"]');
two.insertAdjacentHTML("beforebegin", "<li>beforebegin</li>");
two.insertAdjacentHTML("afterbegin", "<b>afterbegin</b>");
two.insertAdjacentHTML("beforeend", "<b>beforeend</b>");
two.insertAdjacentHTML("afterend", "<li>afterend</li>");
// → 直前に<li>beforebegin</li>、中の先頭に<b>afterbegin</b>、
// 中の末尾に<b>beforeend</b>、直後に<li>afterend</li> が入った
| 位置 | 意味 | 同じ動きのメソッド |
|---|---|---|
beforebegin |
自分の直前 | el.before() |
afterbegin |
自分の中の先頭 | el.prepend() |
beforeend |
自分の中の末尾 | el.append() |
afterend |
自分の直後 | el.after() |
insertAdjacentHTML は既存の子を作り直さずにHTMLを足せるので、innerHTML += より速く動きます。 渡した文字列はHTMLとして解釈されるため、ユーザー入力は入れません。文字として足すなら insertAdjacentText("beforeend", "<b>x</b>") で、結果は みかん<b>x</b> とエスケープされました。
template からまとめて作る
const tpl = document.getElementById("tpl");
const ul = document.getElementById("list");
for (const name of ["キウイ", "なし"]) {
const node = tpl.content.cloneNode(true); // DocumentFragment
node.querySelector(".name").textContent = name;
ul.append(node);
}
[...ul.children].map((e) => e.textContent).join(","); // → "りんご,みかん,ぶどう,キウイ,なし"
HTMLの構造は <template> に書き、値は textContent で入れる形にすると、見通しがよく安全です。
要素を削除・置換・複製する(remove・replaceWith・replaceChildren)
const ul = document.getElementById("list");
// 置換
const lemon = document.createElement("li");
lemon.textContent = "レモン";
ul.querySelector('[data-id="2"]').replaceWith(lemon);
[...ul.children].map((e) => e.textContent).join(","); // → "りんご,レモン,ぶどう"
// 削除
ul.querySelector('[data-id="1"]').remove();
ul.removeChild(lemon).textContent; // → "レモン"
// 子を全部消す・入れ替える
ul.replaceChildren();
ul.children.length; // → 0
const x = document.createElement("li");
x.textContent = "X";
ul.replaceChildren(x, "Y");
ul.innerHTML; // → "<li>X</li>Y"
const li = document.querySelector('[data-id="1"]');
li.cloneNode().outerHTML; // → '<li class="item" data-id="1"></li>'
li.cloneNode(true).outerHTML; // → '<li class="item" data-id="1">りんご</li>'
子を全部消すなら replaceChildren()、入れ替えるなら replaceChildren(新しい子...) が1行で意図も明確です。 cloneNode() は引数なしだと中身なし、true で子孫まで複製します。addEventListener で付けたイベントは複製されません。
属性・data属性・クラス・スタイルを操作する
const li = document.querySelector('[data-id="1"]');
// 属性
li.setAttribute("title", "果物");
li.getAttribute("title"); // → "果物"
li.getAttribute("nope"); // → null
li.removeAttribute("title");
li.hasAttribute("title"); // → false
li.toggleAttribute("hidden"); // → true(付いた)
li.toggleAttribute("hidden", true); // → true(第2引数 true で必ず付ける)
li.toggleAttribute("hidden", false); // → false(false で必ず外す)
// data属性
li.dataset.userName = "taro"; // data-user-name になる
const id = li.dataset.id; // → "1"(typeof は "string")
delete li.dataset.id;
li.outerHTML; // → '<li class="item" data-user-name="taro">りんご</li>'
dataset の値は常に文字列なので、数値として使うときは Number(li.dataset.id) で変換します。 キャメルケース(userName)はケバブケース(data-user-name)に自動変換されます。
const input = document.querySelector('[name="title"]');
input.value = "牛乳";
[input.value, input.getAttribute("value"), input.defaultValue]; // → ["牛乳", "買い物", "買い物"]
入力欄の今の値は value プロパティ、HTMLに書かれた初期値は getAttribute("value")(=defaultValue)です。
classList と toggle の force
const li = document.querySelector('[data-id="1"]');
li.classList.add("a", "b");
li.classList.remove("b");
li.classList.toggle("on"); // → true(付けた)
li.classList.toggle("on"); // → false(外した)
li.classList.toggle("on", true); // → true(必ず付ける)
li.classList.toggle("on", true); // → true(付いたまま)
li.classList.replace("a", "z"); // → true
li.className; // → "item z on"
toggle(クラス名, 条件) は条件が true なら付け、false なら外します。 if で add/remove を書き分けずに、btn.classList.toggle("is-active", count > 0) の1行で済みます。
style と getComputedStyle
const li = document.querySelector('[data-id="1"]');
li.style.backgroundColor = "rgb(255, 0, 0)";
li.style.setProperty("--gap", "8px");
li.style.cssText += "font-size:20px";
li.style.getPropertyValue("--gap"); // → "8px"
getComputedStyle(li).fontSize; // → "20px"
getComputedStyle(li).display; // → "list-item"
li.getAttribute("style");
// → "background-color: rgb(255, 0, 0); --gap: 8px; font-size: 20px;"
el.style はインラインスタイルだけを扱い、CSSファイルの値も含めた最終的な値は getComputedStyle で読みます。 見た目の切り替えは classList でクラスを付け外しし、スタイル自体はCSSに書くと保守しやすくなります。
テキストとHTMLを操作する(textContent・innerText・innerHTML)
const p = document.getElementById("msg");
p.textContent; // → "こんにちは(非表示)"
p.innerText; // → "こんにちは"
p.innerHTML; // → 'こんにちは<span style="display:none">(非表示)</span>'
p.textContent = '<img src=x onerror="window.hit=1">';
p.innerHTML; // → '<img src=x onerror="window.hit=1">'
p.children.length; // → 0(要素は作られない)
| プロパティ | 読むと | 書くと | 向いている用途 |
|---|---|---|---|
textContent |
非表示も含む全テキスト | 文字として入る | 文字の表示(基本はこれ) |
innerText |
画面に見えている文字 | 文字として入る | 見た目どおりの文字を取る |
innerHTML |
中身のHTML文字列 | HTMLとして解析される | 自分で書いた固定のHTML |
outerHTML |
自分を含むHTML文字列 | 自分ごと置き換わる | デバッグ表示など |
ユーザー入力や外部データは textContent で表示します。 タグが文字として扱われ、HTMLとして実行されないためです。XSS対策の全体像は別記事「XSS対策|フレームワーク別の注意点」で扱っています。innerText は読むたびにレイアウト計算が入るため、大量に読むなら textContent が軽く動きます。
親・子・兄弟をたどる(DOMの走査)
const two = document.querySelector('[data-id="2"]');
two.parentElement.id; // → "list"
two.previousElementSibling.textContent; // → "りんご"
two.nextElementSibling.textContent; // → "ぶどう"
two.nextSibling.nodeName; // → "#text"
two.parentElement.lastElementChild.textContent; // → "ぶどう"
| たどる方向 | 要素だけ(おすすめ) | テキストノードも含む |
|---|---|---|
| 親 | parentElement |
parentNode |
| 子 | children |
childNodes |
| 最初・最後の子 | firstElementChild / lastElementChild |
firstChild / lastChild |
| 次・前の兄弟 | nextElementSibling / previousElementSibling |
nextSibling / previousSibling |
| 祖先を条件で | closest(セレクタ) |
― |
nextSibling は改行のテキストノードを返すことがあるので、要素を期待するなら nextElementSibling を使います。
イベントを扱う(addEventListener とオプション)
const ul = document.getElementById("list");
ul.addEventListener("click", (e) => {
console.log("target=" + e.target.dataset.id, "currentTarget=" + e.currentTarget.id);
});
ul.children[0].click(); // → target=1 currentTarget=list
e.target は実際にクリックされた要素、e.currentTarget はリスナーを登録した要素です。 子のクリックは親へ伝わる(バブリング)ので、ul に登録しても li のクリックを受け取れます。
| オプション | 意味 | 使いどころ |
|---|---|---|
once: true |
1回呼ばれたら自動で解除 | 初回だけの案内 |
signal: ac.signal |
ac.abort() でまとめて解除 |
画面を閉じるとき、部品の破棄時 |
passive: true |
preventDefault を呼ばないと宣言 |
scroll・wheel・touchmove を軽く |
capture: true |
親→子の捕獲フェーズで受け取る | 子より先に処理したいとき |
once・signal・removeEventListener
const li = document.querySelector("li");
// once
let n1 = 0;
li.addEventListener("click", () => n1++, { once: true });
li.click(); li.click();
n1; // → 1
// signal(AbortController)
let n2 = 0;
const ac = new AbortController();
li.addEventListener("click", () => n2++, { signal: ac.signal });
li.addEventListener("mouseover", () => (n2 += 10), { signal: ac.signal });
li.click(); // n2 = 1
ac.abort(); // 2つまとめて解除
li.click();
li.dispatchEvent(new MouseEvent("mouseover"));
n2; // → 1
AbortController の signal を渡すと、abort() 1回で複数のリスナーを解除できます。 関数を変数に保存しなくてよいので、アロー関数のまま書けます。
let n = 0;
const li = document.querySelector("li");
li.addEventListener("click", () => n++);
li.removeEventListener("click", () => n++); // 別の関数なので外れない
const h = () => (n += 100);
li.addEventListener("click", h);
li.addEventListener("click", h); // 同じ関数の二重登録は1つにまとまる
li.click(); // +1 +100
li.removeEventListener("click", h);
li.click(); // +1
n; // → 102
removeEventListener は登録したときと同じ関数を渡したときだけ解除されます。
passive: true では preventDefault が効かない
const d = document.createElement("div");
document.body.append(d);
let dp;
d.addEventListener("wheel", (e) => { e.preventDefault(); dp = e.defaultPrevented; }, { passive: true });
d.dispatchEvent(new WheelEvent("wheel", { cancelable: true }));
dp; // → false
// コンソール: Unable to preventDefault inside passive event listener invocation.
同じ処理を passive なしで登録すると defaultPrevented は true でした。passive: true はスクロールを止めないとブラウザに伝える指定で、スクロールがなめらかになります。 独自のスワイプ操作などで止めたいときは passive: false を明示します。
イベントの伝わる順番(捕獲・バブリング)
const out = [];
const ul = document.getElementById("list");
const li = ul.children[0];
ul.addEventListener("click", () => out.push("ul:capture"), { capture: true });
ul.addEventListener("click", () => out.push("ul:bubble"));
li.addEventListener("click", () => out.push("li"));
document.addEventListener("click", () => out.push("document"));
li.click();
out.push("|");
li.addEventListener("click", (e) => e.stopPropagation());
li.click();
out.join(" > ");
// → "ul:capture > li > ul:bubble > document > | > ul:capture > li"
イベントは外側から内側(捕獲)→内側から外側(バブリング)の順に伝わり、stopPropagation() でその先の親へは届かなくなります。 親でまとめて処理するイベント委譲と組み合わせにくいので、使う場面は絞ります。独自イベント new CustomEvent(名前, { detail, bubbles: true }) は既定ではバブリングしないため、親で受け取るなら bubbles: true を付けます。
イベント委譲(親で1回登録して子をまとめて処理)
イベント委譲とは、親要素にリスナーを1つだけ登録し、e.target.closest() でどの子がクリックされたかを判定する書き方です。
const out = [];
const ul = document.getElementById("list");
ul.addEventListener("click", (e) => {
const li = e.target.closest("li.item");
if (!li || !ul.contains(li)) return; // ul自体の余白などは無視
out.push(li.dataset.id);
});
// 後から追加した要素(中に<b>がある)
ul.insertAdjacentHTML("beforeend", '<li class="item" data-id="4"><b>メロン</b></li>');
ul.children[0].click(); // li を直接
ul.querySelector("b").click(); // li の中の <b>
ul.click(); // ul 自体
out; // → ["1", "4"]
| 比べる点 | 子ごとに登録 | イベント委譲 |
|---|---|---|
| リスナーの数 | 子の数だけ | 1つ |
| 後から追加した要素 | 追加のたびに登録 | 自動で対象 |
子の中の <b> をクリック |
e.target が <b> になる |
closest で li にたどれる |
| 向いている場面 | 子が少なく固定 | 一覧・表・動的に増える要素 |
e.target.dataset.id と書くと、中の <b> がクリックされたとき undefined になります。 closest で目的の要素までたどると、どこをクリックしても同じように扱えます。
フォームを操作する(FormData・submit)
const f = document.getElementById("f");
Object.fromEntries(new FormData(f)); // → { title: "買い物", pri: "high", done: "on" }
f.elements.title.value; // → "買い物"
f.elements.pri.value; // → "high"
f.elements.done.checked; // → true
const out = [];
f.addEventListener("submit", (e) => {
e.preventDefault(); // ページ遷移を止める
out.push("submit:" + (e.submitter ? e.submitter.textContent : null));
});
f.querySelector("button").click(); // ボタンで送信
f.requestSubmit(); // コードから送信
f.addEventListener("input", (e) => out.push("input:" + e.target.name));
f.elements.title.value = "x";
f.elements.title.dispatchEvent(new Event("input", { bubbles: true }));
out; // → ["submit:送信", "submit:null", "input:title"]
new FormData(form) でname属性を持つ入力欄をまとめて取り出せます。 チェックボックスはチェック時に "on"(value属性があればその値)、未チェックなら項目ごと含まれないので、真偽値は checked で読みます。同じnameが複数あるときは getAll("name") を使います。
送信処理はボタンの click ではなくフォームの submit に書くと、Enterキーでの送信も同じ場所で受け取れます。 requestSubmit() は submit イベントと入力チェック(required など)を通して送信し、form.submit() はそれらを通さずに送信します。
大量の要素を追加するときの速さ(DocumentFragmentの計測)
「DocumentFragmentを使うと速い」とよく言われますが、差が大きく出るのは「追加の途中でレイアウト情報を読む」場合でした。 N件の li を追加し、最後に ul.offsetHeight でレイアウトを確定させるまでを、各9回実行した中央値です(Chromium 141 ヘッドレス、共有コンテナのため値はばらつきます。傾向の目安としてご覧ください)。
| 方法 | 件数 | 中央値 |
|---|---|---|
ループ内で ul.appendChild(li) |
10,000 | 290.7ms |
| DocumentFragmentに貯めて1回追加 | 10,000 | 490.8ms |
配列に貯めて ul.append(...arr) |
10,000 | 465.8ms |
文字列を連結して innerHTML に1回代入 |
10,000 | 287.7ms |
innerHTML += をループで繰り返す |
2,000 | 4,201.8ms |
ループ内で appendChild + 毎回 offsetHeight を読む |
1,000 | 730.7ms |
| DocumentFragment + 最後に1回だけ読む | 1,000 | 27.9ms |
- 途中でレイアウトを読まなければ、ループ内の
appendChildでも十分速い結果でした(今回は fragment との差はばらつきの範囲内)。ブラウザが描画をまとめて行うためです。 - 追加のたびに
offsetHeightやgetBoundingClientRect()を読むと、毎回レイアウト計算が走り約26倍の時間がかかりました。 読み取りはループの外にまとめます。 innerHTML +=の繰り返しは毎回中身を全部作り直すため、2,000件で約4秒かかりました。 末尾に足すならinsertAdjacentHTML("beforeend", ...)を使います。
| 状況 | おすすめ |
|---|---|
| 一覧をまとめて描画 | append(...nodes) または DocumentFragment |
| 一覧を全部入れ替える | replaceChildren(...nodes) |
| HTML文字列を末尾に足す | insertAdjacentHTML("beforeend", html) |
| 追加しながら高さを測りたい | 先に全部追加 → 最後にまとめて測る |
よくあるエラーと直し方(実際のエラー文)
| エラー文(Chromium 141) | 主な原因 | 直し方 |
|---|---|---|
TypeError: Cannot read properties of null (reading 'textContent') |
要素が見つからず null |
セレクタ確認、defer、?. |
TypeError: document.querySelectorAll(...).addEventListener is not a function |
NodeList に直接登録 | forEach で1つずつ、またはイベント委譲 |
document.getElementsByTagName(...).forEach is not a function |
HTMLCollection に forEach はない |
[...list].forEach / for...of |
TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'. |
文字列を appendChild |
append を使う |
DOMException: Failed to execute 'querySelector' on 'Document': '#1' is not a valid selector. |
数字で始まるid | "#" + CSS.escape("1") / getElementById |
DOMException: Failed to execute 'append' on 'Element': The new child element contains the parent. |
親を自分の子に入れようとした | 追加先と対象を見直す |
DOMException: Failed to set a named property 'user-name' on 'DOMStringMap': 'user-name' is not a valid property name. |
dataset にケバブケース | dataset.userName と書く |
Cannot read properties of null(HTMLより先にスクリプトが動いた)
<head>
<script>window.r1 = document.getElementById("box");</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
window.r2 = document.getElementById("box").id;
});
</script>
</head>
<body><div id="box"></div></body>
<!-- 結果: r1 → null、r2 → "box" -->
<script defer>(または type="module")にするか、DOMContentLoaded の中で取得すると、HTMLの読み込み後に実行されます。 要素がない場合もあり得るなら、document.querySelector(".toast")?.remove() のようにオプショナルチェーンで書くとエラーになりません。
ライブなリストをループしながら削除すると残る
// 直す前
const live = document.getElementsByClassName("item");
for (let i = 0; i < live.length; i++) live[i].remove();
live.length; // → 1(1つ残る)
// 直した後(配列にコピーしてから回す)
const live2 = document.getElementsByClassName("item");
for (const el of [...live2]) el.remove();
live2.length; // → 0
ライブなリストは削除のたびに詰まるため、インデックスで回すと1つおきに飛ばされます。 [...list] で配列にしてから回すか、静的な querySelectorAll を使います。
よくある質問
Q. querySelector と getElementById はどちらを使えばよいですか?
A. 統一感を重視するなら querySelector / querySelectorAll で揃えるのがおすすめです。 CSSと同じセレクタで何でも取れるためです。idで1つ取るだけなら getElementById も分かりやすく、数字で始まるidもそのまま扱えます。
Q. NodeList と HTMLCollection の違いは何ですか?
A. querySelectorAll は静的なNodeList(forEach あり)、getElementsByClassName や children はライブなHTMLCollection(forEach なし)です。 どちらも配列ではないので、map などは [...list] にしてから使います。
Q. 要素を削除したら、登録したイベントも消えますか?
A. 要素がどこからも参照されなくなれば、リスナーも一緒に回収されます。 window や document に登録したものは残るため、AbortController の signal を渡しておき、不要になったら abort() でまとめて解除すると管理しやすくなります。
Q. innerHTML は使わないほうがよいのですか?
A. 自分で書いた固定のHTMLを入れる用途なら便利に使えます。 ユーザー入力や外部データは textContent、構造が複雑なら <template> + textContent にすると、HTMLとして解釈される心配がなく読みやすくなります。
まとめ
- 取得は
querySelector/querySelectorAll、祖先はclosest。見つからなければnullか空のNodeList querySelectorAllは静的、getElementsBy...とchildrenはライブ。ループ中の削除は配列にコピーしてから- 追加は
append/prepend/before/after、削除はremove、入れ替えはreplaceWith/replaceChildren - 文字の表示は
textContent。HTMLを足すならinsertAdjacentHTML、ひな形は<template> - クラスは
classList.toggle(名前, 条件)、data属性はdataset(値は文字列) - 一覧のイベントは委譲(親で1回登録+
e.target.closest())、解除はAbortControllerが手軽 - 大量追加で時間がかかるのは「途中でレイアウトを読む」「
innerHTML +=の繰り返し」。読み取りは最後にまとめる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScript DOM操作一覧|querySelector〜イベント・コピペ可」
株式会社RJCでは、JavaScriptのDOM操作を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。