「JavaScriptの基礎 - DOM操作」の版間の差分
ページの作成:「== 概要 == DOM (Document Object Model) は、HTML または XMLドキュメントをプログラムから操作するためのAPIである。<br> <br> WebブラウザはHTMLを解析し、各要素をノードオブジェクトとして表現した木構造 (DOMツリー) をメモリ上に構築する。<br> JavaScriptはこのDOMツリーにアクセスし、要素の取得・作成・追加・削除、属性やスタイルの変更、ツリーの走査といっ…」 |
|||
| 528行目: | 528行目: | ||
| 使用可能(推奨) || 非推奨(大文字が小文字に正規化される場合がある) | | 使用可能(推奨) || 非推奨(大文字が小文字に正規化される場合がある) | ||
|} | |} | ||
</center> | |||
<br> | <br> | ||
<syntaxhighlight lang="html"> | <syntaxhighlight lang="html"> | ||
| 568行目: | 569行目: | ||
</center> | </center> | ||
<br> | <br> | ||
==== クラス操作 (classList) ==== | ==== クラス操作 (classList) ==== | ||
<code>classList</code> プロパティは、要素のCSSクラスを操作するためのメソッドを提供する。<br> | <code>classList</code> プロパティは、要素のCSSクラスを操作するためのメソッドを提供する。<br> | ||
2026年2月22日 (日) 00:12時点における版
概要
DOM (Document Object Model) は、HTML または XMLドキュメントをプログラムから操作するためのAPIである。
WebブラウザはHTMLを解析し、各要素をノードオブジェクトとして表現した木構造 (DOMツリー) をメモリ上に構築する。
JavaScriptはこのDOMツリーにアクセスし、要素の取得・作成・追加・削除、属性やスタイルの変更、ツリーの走査といった操作を行う。
主要なDOM操作は以下の通りである。
| 機能 | 説明 |
|---|---|
| 要素の取得 | querySelector / getElementById 等のメソッドで、DOMツリーから特定の要素を取得する。
|
| 要素の作成・追加・削除 | createElement で要素を生成し、append / remove 等でDOMツリーを変更する。
|
| テキスト・HTMLコンテンツの操作 | textContent / innerHTML 等で要素のコンテンツを読み書きする。
|
| 属性・スタイル操作 | setAttribute / classList / style 等でHTML属性やCSSを操作する。
|
| DOM走査 | parentElement / children / closest 等でツリーを移動する。
|
なお、React等のUIフレームワークは仮想DOMを介して実際のDOM操作を最小限に抑えるため、開発者がDOMを直接操作することは原則として無い。
バニラJavaScriptにおけるDOM操作の理解は、フレームワークの動作原理を把握する上でも重要である。
DOMの基本
DOMツリー
WebブラウザはHTMLドキュメントを読み込むと、その構造をメモリ上にツリー状のオブジェクト (DOMツリー) として構築する。
このDOMツリーは、HTML要素の親子関係・兄弟関係をそのまま反映している。
例えば、以下に示すようなHTMLがあるとする。
<html>
<head>
<title>ページタイトル</title>
</head>
<body>
<h1>見出し</h1>
<p>段落テキスト</p>
</body>
</html>
このHTMLは、以下に示すようなDOMツリーに変換される。
Document
└── html (ELEMENT_NODE)
├── head (ELEMENT_NODE)
│ └── title (ELEMENT_NODE)
│ └── "ページタイトル" (TEXT_NODE)
└── body (ELEMENT_NODE)
├── h1 (ELEMENT_NODE)
│ └── "見出し" (TEXT_NODE)
└── p (ELEMENT_NODE)
└── "段落テキスト" (TEXT_NODE)
DOMツリーの各要素はノードと呼ばれ、JavaScriptからアクセスおよび操作が可能である。
ノードの種類
DOMツリーの各ノードは nodeType プロパティを持ち、ノードの種類を数値で表す。
下表に、主なノード型を示す。
| nodeType値 | 定数名 | 説明 |
|---|---|---|
| 1 | ELEMENT_NODE | HTML要素ノード (<div>, <p> 等)
|
| 3 | TEXT_NODE | テキストノード (要素内のテキスト内容) |
| 8 | COMMENT_NODE | コメントノード (<!-- ... -->)
|
| 9 | DOCUMENT_NODE | ドキュメントノード (document オブジェクト)
|
| 10 | DOCUMENT_TYPE_NODE | DOCTYPE宣言ノード |
| 11 | DOCUMENT_FRAGMENT_NODE | ドキュメントフラグメントノード |
const element = document.querySelector('p')
console.log(element.nodeType) // 1 (ELEMENT_NODE)
console.log(element.firstChild.nodeType) // 3 (TEXT_NODE)
console.log(element.nodeName) // "P"
console.log(element.firstChild.nodeValue) // テキスト内容
通常のDOM操作では、要素ノード (ELEMENT_NODE) を主に扱うが、テキストノードやコメントノードの存在も理解しておく必要がある。
要素の取得
DOMツリーから特定の要素を取得するためのメソッドが複数存在する。
querySelector / querySelectorAll
querySelector と querySelectorAll は、CSSセレクタを使用して要素を取得するメソッドである。
柔軟なセレクタ指定が可能であり、現在最も推奨される取得方法である。
querySelector は、セレクタに一致する最初の要素を返す。
一致する要素がない場合は null を返す。
// IDで取得
const header = document.querySelector('#header')
// クラスで取得
const item = document.querySelector('.item')
// 複合セレクタ
const input = document.querySelector('div.form-group input[type="text"]')
// 擬似クラス
const firstItem = document.querySelector('li:first-child')
querySelectorAll は、セレクタに一致する全ての要素を静的な NodeList として返す。
一致する要素がない場合は空の NodeList を返す。
// 全ての段落を取得
const paragraphs = document.querySelectorAll('p')
// 複数セレクタ (カンマ区切り)
const elements = document.querySelectorAll('div.note, div.alert')
// forEachで反復処理
paragraphs.forEach(p => {
console.log(p.textContent)
})
querySelectorAll が返す NodeList は静的 (non-live) であり、取得後にDOMが変更されても NodeList の内容は更新されない。
forEach メソッドが使用可能であるため、反復処理が容易である。
getElementById / getElementsByClassName / getElementsByTagName
これらは従来から存在する要素取得メソッドである。
getElementById は、指定されたIDに一致する単一の要素を返す。
document オブジェクトでのみ使用可能であり、一致する要素がない場合は null を返す。
const element = document.getElementById('main-content')
getElementsByClassName と getElementsByTagName は、動的な HTMLCollection を返す。
// クラス名で取得 (動的HTMLCollection)
const items = document.getElementsByClassName('item')
// 複数クラスを指定 (両方を持つ要素のみ)
const activeItems = document.getElementsByClassName('item active')
// タグ名で取得 (動的HTMLCollection)
const paragraphs = document.getElementsByTagName('p')
// 特定要素の子孫から検索
const container = document.getElementById('container')
const buttons = container.getElementsByTagName('button')
動的HTMLCollectionは、DOMの変更に追従して自動的に更新される。
そのため、反復中にDOMを追加・削除すると無限ループ等の予期しない動作が発生する可能性がある。
反復処理を行う場合は、Array.from() で静的なコピーを作成することを推奨する。
// 動的HTMLCollectionでの反復は注意が必要
const items = document.getElementsByClassName('item')
// Array.from()で静的コピーを作成して安全に反復
Array.from(items).forEach(item => {
// DOM操作を安全に行える
})
取得メソッドの比較
各取得メソッドの特性を以下に示す。
| メソッド | 引数 | 戻り値 | 動的 / 静的 |
|---|---|---|---|
querySelector |
CSSセレクタ | Element / null | - |
querySelectorAll |
CSSセレクタ | NodeList | 静的 |
getElementById |
ID文字列 | Element / null | - |
getElementsByClassName |
クラス名 | HTMLCollection | 動的 |
getElementsByTagName |
タグ名 | HTMLCollection | 動的 |
新規開発では querySelector / querySelectorAll の使用を推奨する。
CSSセレクタによる柔軟な指定が可能であり、静的な NodeList を返すため安全に操作できる。
要素の作成・追加・削除
要素の作成
新しいDOM要素を作成するには、document.createElement メソッドを使用する。
// 要素の作成
const div = document.createElement('div')
const p = document.createElement('p')
const a = document.createElement('a')
// 作成した要素に属性やテキストを設定
div.id = 'new-container'
div.className = 'container'
p.textContent = '新しい段落のテキスト'
a.href = 'https://example.com'
a.textContent = 'リンクテキスト'
テキストノードを独立して作成する場合は、document.createTextNode を使用する。
const textNode = document.createTextNode('テキスト内容')
createElement で作成された要素はメモリ上に存在するのみであり、DOMツリーに追加しない限りページ上には表示されない。
要素の追加
新しいAPI (append / prepend / before / after)
ES2015以降で追加された新しいAPIでは、複数の引数を受け取り、文字列も直接追加できる。
| メソッド | 挿入位置 | 説明 |
|---|---|---|
append() |
子要素の末尾 | 最後の子要素として追加する |
prepend() |
子要素の先頭 | 最初の子要素として追加する |
before() |
要素の前 | 兄弟要素として前に追加する |
after() |
要素の後 | 兄弟要素として後に追加する |
const parent = document.querySelector('#container')
const newElement = document.createElement('p')
newElement.textContent = '新しい要素'
// 最後の子要素として追加
parent.append(newElement)
// 文字列を直接追加 (自動的にTextNodeに変換)
parent.append('テキスト')
// 複数の要素とテキストを一度に追加
parent.append(newElement, 'テキスト', document.createElement('br'))
// 最初の子要素として追加
parent.prepend(newElement)
// 兄弟として前後に追加
const ref = document.querySelector('#reference')
ref.before(newElement)
ref.after(newElement)
従来のAPI (appendChild / insertBefore)
従来のAPIは Node オブジェクトのみを引数として受け取る。
const parent = document.querySelector('#container')
const child = document.createElement('div')
// 最後の子ノードとして追加
parent.appendChild(child)
// 参照ノードの前に挿入
const ref = document.querySelector('#reference')
parent.insertBefore(child, ref)
新旧APIの違いを以下に示す。
| 項目 | 新しいAPI (append等) | 従来のAPI (appendChild等) |
|---|---|---|
| 文字列対応 | 可能 (自動的にTextNodeに変換) | Nodeオブジェクトのみ |
| 複数引数 | 対応 | 単一引数のみ |
| 戻り値 | undefined |
追加されたNode |
要素の削除
要素をDOMツリーから削除するメソッドを以下に示す。
// remove() : 要素自身をDOMから削除
const element = document.querySelector('#target')
element.remove()
// removeChild() : 子ノードを親から削除 (従来のAPI)
const parent = document.querySelector('#container')
const child = document.querySelector('#target')
parent.removeChild(child)
remove() は要素自身を直接削除でき、親要素の参照が不要であるため簡潔に記述できる。
DocumentFragment
多数の要素をDOMに追加する場合、1つずつ追加するとその都度ブラウザのリフロー (レイアウト再計算) が発生し、パフォーマンスが低下する。
DocumentFragment を使用することにより、複数の要素をまとめて1回のDOM更新で追加できる。
const list = document.querySelector('ul')
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li')
item.textContent = `Item ${i}`
fragment.appendChild(item) // DocumentFragmentに追加 (リフローなし)
}
list.appendChild(fragment) // 1回のDOM更新で全要素を追加
append メソッドの複数引数を利用する方法もある。
const list = document.querySelector('ul')
const items = []
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li')
item.textContent = `Item ${i}`
items.push(item)
}
list.append(...items) // スプレッド構文で一度に追加
テキスト・HTMLコンテンツの操作
textContent / innerText / innerHTML
要素のテキストやHTMLコンテンツを操作するプロパティが3つ存在する。
それぞれの特性が異なるため、用途に応じた使い分けが重要である。
| 項目 | textContent | innerText | innerHTML |
|---|---|---|---|
| 用途 | プレーンテキストの取得 / 設定 | 表示テキストの取得 / 設定 | HTMLマークアップの取得/設定 |
| 非表示要素の扱い | 取得する | 取得しない (CSSを考慮) | HTMLとして取得する |
| script / style要素 | テキストとして取得する | 取得しない | HTMLとして取得する |
| XSSリスク | 安全 | 安全 | 危険 |
| パフォーマンス | 高速 | 低速 (CSSレイアウト計算が必要) | HTMLパーサーの呼び出しが必要 |
const element = document.querySelector('#content')
// textContent: プレーンテキストとして取得 / 設定
const text = element.textContent
element.textContent = '新しいテキスト'
// innerText: 表示されているテキストのみ取得 / 設定
const visibleText = element.innerText
// innerHTML: HTMLマークアップを含めて取得 / 設定
const html = element.innerHTML
element.innerHTML = '<p>新しい<strong>HTML</strong>コンテンツ</p>'
textContent を設定すると、要素の全ての子ノードが削除され、単一のテキストノードに置き換わる。
innerHTML はXSS (クロスサイトスクリプティング) 攻撃の脆弱性となるため、ユーザ入力を含む文字列を設定してはならない。
テキストの設定には、textContent を使用すること。
// ユーザ入力をinnerHTMLに設定するのは危険
const userInput = '<img src="x" onerror="alert(\'XSS\')">'
element.innerHTML = userInput // スクリプトが実行される
// textContentを使用すればプレーンテキストとして安全に設定される
element.textContent = userInput // そのまま文字列として表示される
insertAdjacentHTML
insertAdjacentHTML は、指定したHTML文字列をDOMツリーの指定位置に挿入するメソッドである。
既存の要素を破壊せずにHTMLを挿入できるため、innerHTML による再設定より効率的である。
4つの挿入ポジションを以下に示す。
| ポジション | 挿入位置 |
|---|---|
beforebegin |
要素自身の直前 (兄弟として) |
afterbegin |
要素の先頭 (最初の子要素の前) |
beforeend |
要素の末尾 (最後の子要素の後) |
afterend |
要素自身の直後 (兄弟として) |
<!-- beforebegin -->
<p>
<!-- afterbegin -->
content
<!-- beforeend -->
</p>
<!-- afterend -->
const element = document.querySelector('#target')
element.insertAdjacentHTML('beforebegin', '<div>要素の前</div>')
element.insertAdjacentHTML('afterbegin', '<span>先頭に挿入</span>')
element.insertAdjacentHTML('beforeend', '<span>末尾に挿入</span>')
element.insertAdjacentHTML('afterend', '<div>要素の後</div>')
insertAdjacentHTML もHTMLを解析するため、ユーザ入力の挿入にはXSSのリスクがある。
プレーンテキストの挿入には insertAdjacentText を使用すること。
属性操作
標準的な属性操作
下表に、要素の属性を操作する基本的なメソッドを示す。
| メソッド | 説明 |
|---|---|
getAttribute(name) |
指定した属性の値を文字列で取得する。 |
setAttribute(name, value) |
属性を設定する。(存在しない場合は新規作成) |
hasAttribute(name) |
属性が存在するかを boolean で返す。 |
removeAttribute(name) |
属性を削除する。 |
const link = document.querySelector('a')
// 属性の取得
const href = link.getAttribute('href')
// 属性の設定
link.setAttribute('href', 'https://example.com')
link.setAttribute('target', '_blank')
// 属性の存在確認
if (link.hasAttribute('target')) {
console.log('target属性が存在する')
}
// 属性の削除
link.removeAttribute('target')
// boolean属性 (disabled, readonly等) の設定
const button = document.querySelector('button')
button.setAttribute('disabled', '') // 無効化
button.removeAttribute('disabled') // 有効化
data属性 (dataset)
HTML5の data-* カスタムデータ属性は、dataset プロパティを使用してアクセスする。
属性名は、ケバブケースからキャメルケースに自動変換される。
| 項目 | ケバブケース (kebab-case) | キャメルケース (camelCase / PascalCase) |
|---|---|---|
| 区切り文字 | ハイフン `-` | なし(大文字で区切る) |
| 先頭文字 | 小文字 | 小文字(lowerCamelCase)または大文字(UpperCamelCase / PascalCase) |
| 記述例 | `my-variable-name` | `myVariableName` / `MyVariableName` |
| 主な用途 | CSS クラス名・ID、URL スラッグ、HTML 属性、ファイル名 | 変数名・関数名(Java, C#, JavaScript 等)、クラス名(PascalCase) |
| 言語・環境の例 | HTML, CSS, Lisp 系, REST API のエンドポイント | C#, Java, JavaScript, TypeScript, Swift |
| 大文字・小文字の区別 | 全て小文字が基本 | 単語の先頭を大文字にする。 |
| スペースの代替 | ハイフンがスペース相当 | 大文字がスペース相当 |
| 視認性 | ハイフンにより単語の区切りが明確 | 慣れるまで単語の区切りが読みにくい場合がある。 |
| URLでの使用 | 使用可能(推奨) | 非推奨(大文字が小文字に正規化される場合がある) |
<div id="user" data-user-id="123" data-user-name="John" data-date-of-birth="1990-01-01">
John
</div>
const el = document.querySelector('#user')
// 読み取り (ケバブケース → キャメルケース)
console.log(el.dataset.userId) // "123" (data-user-id)
console.log(el.dataset.userName) // "John" (data-user-name)
console.log(el.dataset.dateOfBirth) // "1990-01-01" (data-date-of-birth)
// 設定
el.dataset.role = 'admin' // data-role="admin" が追加される
// 削除
delete el.dataset.dateOfBirth
// 存在確認
if ('userId' in el.dataset) {
console.log('userId属性が存在する')
}
| HTML属性名 | JavaScriptプロパティ名 |
|---|---|
| data-id | dataset.id |
| data-user-name | dataset.userName |
| data-date-of-birth | dataset.dateOfBirth |
クラス操作 (classList)
classList プロパティは、要素のCSSクラスを操作するためのメソッドを提供する。
| メソッド | 説明 |
|---|---|
add(class1, class2, ...) |
1つ以上のクラスを追加する。 |
remove(class1, class2, ...) |
1つ以上のクラスを削除する。 |
toggle(class) |
クラスが存在すれば削除し、存在しなければ追加する。 |
toggle(class, force) |
force が true なら追加、false なら削除する。
|
contains(class) |
クラスが存在するかを boolean で返す |
replace(oldClass, newClass) |
クラスを別のクラスに置換する |
const element = document.querySelector('#target')
// クラスの追加
element.classList.add('active')
element.classList.add('highlight', 'visible') // 複数クラスを一度に追加
// クラスの削除
element.classList.remove('active')
element.classList.remove('highlight', 'visible')
// クラスのトグル
element.classList.toggle('active') // 追加 / 削除を切り替え
element.classList.toggle('active', isActive) // 条件に基づいて追加 / 削除
// クラスの存在確認
if (element.classList.contains('active')) {
console.log('activeクラスが存在する')
}
// クラスの置換
element.classList.replace('old-class', 'new-class')
スタイル操作
要素のスタイルを操作する方法として、element.style プロパティと getComputedStyle 関数がある。
element.style はインラインスタイル (style 属性) のみを操作する。
CSSプロパティ名はケバブケースからキャメルケースに変換して使用する。
const element = document.querySelector('#target')
// スタイルの設定 (ケバブケース -> キャメルケース)
element.style.color = 'blue'
element.style.fontSize = '18px' // font-size
element.style.backgroundColor = '#f0f0f0' // background-color
element.style.borderTopWidth = '2px' // border-top-width
// スタイルの削除 (空文字列を設定)
element.style.color = ''
getComputedStyle は、外部スタイルシートを含む全てのCSSプロパティの計算済み値を取得する読み取り専用の関数である。
const element = document.querySelector('#target')
const styles = window.getComputedStyle(element)
// 計算済みスタイルの取得
const fontSize = styles.getPropertyValue('font-size')
const color = styles.getPropertyValue('color')
console.log(fontSize) // "16px" (外部CSSも含めた実際の値)
// 擬似要素のスタイル取得
const afterStyles = window.getComputedStyle(element, '::after')
const content = afterStyles.getPropertyValue('content')
| 項目 | element.style | getComputedStyle() |
|---|---|---|
| 対象範囲 | インラインスタイルのみ | 全てのCSSプロパティ (外部スタイル含む) |
| 読み書き | 読み書き可能 | 読み取り専用 |
| 値の種類 | インラインスタイルの値 | Webブラウザが計算した最終的な値 |
視覚的なスタイル変更は classList によるCSSクラスの切り替えが推奨される。
element.style は動的に計算が必要な値 (アニメーションの座標等) の設定に適している。
DOM走査
親・子・兄弟要素のアクセス
DOMツリーを移動するためのプロパティとして、Element系とNode系の2種類がある。
Element系はHTML要素ノードのみを対象とし、Node系はテキストノードやコメントノードを含む全てのノードを対象とする。
| 操作 | Element系 (要素のみ) | Node系 (全ノード) |
|---|---|---|
| 親 | parentElement | parentNode |
| 子 (コレクション) | children (HTMLCollection) | childNodes (NodeList) |
| 最初の子 | firstElementChild | firstChild |
| 最後の子 | lastElementChild | lastChild |
| 次の兄弟 | nextElementSibling | nextSibling |
| 前の兄弟 | previousElementSibling | previousSibling |
const element = document.querySelector('#target')
// 親要素
const parent = element.parentElement
// 子要素
const children = element.children // HTMLCollection (要素のみ)
const firstChild = element.firstElementChild // 最初の子要素
const lastChild = element.lastElementChild // 最後の子要素
// 兄弟要素
const next = element.nextElementSibling // 次の兄弟要素
const prev = element.previousElementSibling // 前の兄弟要素
HTML内の改行やスペースはテキストノードとして解析されるため、Node系のプロパティ (firstChild 等) では予期しないテキストノードが取得される場合がある。
通常は、Element系のプロパティ (firstElementChild 等) の使用を推奨する。
closest
closest メソッドは、指定したCSSセレクタに一致する最も近い祖先要素 (またはその要素自身) を返す。
要素自身から始まり、DOMツリーを上方向に走査する。
一致する要素がない場合は null を返す。
// HTML : <article> > <div id="outer"> > <div id="inner"> > <p id="target">
const target = document.querySelector('#target')
target.closest('div') // <div id="inner"> (最も近いdiv)
target.closest('#outer') // <div id="outer">
target.closest('article') // <article>
target.closest('section') // null (一致なし)
target.closest(':not(div)') // <article> (divでない最も近い祖先)
closest はイベント委譲 (Event Delegation) で頻繁に使用されるメソッドである。
イベント委譲の詳細についてはJavaScriptの基礎 - イベント(DOM)を参照すること。
// イベント委譲での典型的なclosestの使用例
document.addEventListener('click', event => {
const button = event.target.closest('button')
if (button) {
// ボタンまたはその子要素がクリックされた場合の処理
console.log(button.dataset.action)
}
})
ReactがDOMを直接操作しない理由
React等のモダンフレームワークでは、開発者がDOMを直接操作することは原則としてない。
この方針には、いくつかの重要な理由がある。
仮想DOM
Reactは仮想DOMと呼ばれる仕組みを採用している。
仮想DOMはDOMの軽量なインメモリ表現であり、実際のDOM操作の前に変更差分を計算して、最小限の実DOM更新のみを実行する。
この仕組みにより、開発者は UIがどのような状態であるべきか を宣言的に記述するだけでよく、実際のDOM操作はReactが最適化して処理する。
命令型と宣言型の対比
直接DOM操作は命令型のアプローチである。
何をどのように変更するか をステップごとに記述する必要があり、状態が増えるほどUIとの同期が複雑化する。
一方、Reactは宣言型のアプローチを採用しており、この状態のときにUIはこうあるべき と記述する。
状態が変化すると、Reactが自動的にDOMの差分を計算して更新する。
直接DOM操作の問題点
大規模なアプリケーションで直接DOM操作を行う場合、以下に示すような問題が発生しやすい。
- 状態の不整合
- JavaScriptの変数が保持する状態とUIの表示が乖離するリスクがある。
- 複数箇所からDOMを操作すると、整合性の維持が困難になる。
- パフォーマンスの非効率性
- 不必要なDOM操作やリフロー・リペイントの増加が発生しやすい。
- 個々の操作では最適化が困難である。
- 保守性の低下
- 状態変更とUI更新の関連性が把握しにくくなる。
- コードの複雑化に伴い、バグが増加する傾向がある。
ただし、React開発においても ref を使用して直接DOM要素にアクセスする場面 (フォーカス管理、スクロール制御、外部ライブラリとの統合等) は存在する。
そのため、DOMの基本操作を理解しておくことは重要である。
関連情報