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>") で、結果は みかん&lt;b&gt;x&lt;/b&gt; とエスケープされました。

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;       // → '&lt;img src=x onerror="window.hit=1"&gt;'
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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?