JavaScriptの基礎 - DOM操作

提供: MochiuWiki : SUSE, EC, PCB

2026年2月22日 (日) 00:13時点におけるWiki (トーク | 投稿記録)による版 (data属性 (dataset))
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

概要

DOM (Document Object Model) は、HTML または XMLドキュメントをプログラムから操作するためのAPIである。

WebブラウザはHTMLを解析し、各要素をノードオブジェクトとして表現した木構造 (DOMツリー) をメモリ上に構築する。
JavaScriptはこのDOMツリーにアクセスし、要素の取得・作成・追加・削除、属性やスタイルの変更、ツリーの走査といった操作を行う。

主要な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

querySelectorquerySelectorAll は、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')


getElementsByClassNamegetElementsByTagName は、動的な 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の比較
項目 新しい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つ存在する。
それぞれの特性が異なるため、用途に応じた使い分けが重要である。

テキスト・HTMLコンテンツ操作プロパティの比較
項目 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つの挿入ポジションを以下に示す。

insertAdjacentHTMLのポジション
ポジション 挿入位置
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属性が存在する')
 }


data属性の名前変換規則
HTML属性名 JavaScriptプロパティ名
data-id dataset.id
data-user-name dataset.userName
data-date-of-birth dataset.dateOfBirth


クラス操作 (classList)

classList プロパティは、要素のCSSクラスを操作するためのメソッドを提供する。

classListのメソッド
メソッド 説明
add(class1, class2, ...) 1つ以上のクラスを追加する。
remove(class1, class2, ...) 1つ以上のクラスを削除する。
toggle(class) クラスが存在すれば削除し、存在しなければ追加する。
toggle(class, force) forcetrue なら追加、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 の比較
項目 element.style getComputedStyle()
対象範囲 インラインスタイルのみ 全てのCSSプロパティ (外部スタイル含む)
読み書き 読み書き可能 読み取り専用
値の種類 インラインスタイルの値 Webブラウザが計算した最終的な値


視覚的なスタイル変更は classList によるCSSクラスの切り替えが推奨される。
element.style は動的に計算が必要な値 (アニメーションの座標等) の設定に適している。


DOM走査

親・子・兄弟要素のアクセス

DOMツリーを移動するためのプロパティとして、Element系とNode系の2種類がある。

Element系はHTML要素ノードのみを対象とし、Node系はテキストノードやコメントノードを含む全てのノードを対象とする。

DOM走査プロパティの比較
操作 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の基本操作を理解しておくことは重要である。


関連情報