JavaScript実践 #1 DOM操作の基本

読了 7分

JavaScriptトラックの最後 — 実践シリーズです。基礎/中級/上級で鍛えたツールを束ねてバニラJavaScriptで動的なWebページを作ります。

全6編で構成されます。

  • #1 DOM操作の基本 ← 今回の記事
  • #2 イベントハンドリングと委譲
  • #3 fetch と非同期 UI
  • #4 フォームを扱う — validation、FormData
  • #5 ローカルストレージと軽量な状態管理
  • #6 小さなアプリのビルド — Todo またはライブ検索

今回の記事では DOMを触る基本 を扱います — ページのエレメントを探し、内容/属性/クラスを変え、新しいエレメントを作って差し込む場面です。

DOMとは? #

DOM (Document Object Model) は、ブラウザがHTMLをJavaScriptが扱えるオブジェクトツリーに変換したものです。<h1><p> のようなタグがすべてJavaScriptオブジェクトになります。

シンプルな HTML
<body>
  <h1 id="title">こんにちは</h1>
  <p class="message">本文</p>
</body>

これをJavaScriptから見ると、次のようなツリー構造になります。

DOM ツリー
document
  └─ html
      └─ body
          ├─ h1#title  "こんにちは"
          └─ p.message  "本文"

document がエントリポイントで、ツリー構造ですべてのエレメントにアクセスできます。

エレメントを探す — querySelector ファミリー #

最もよく使うメソッド。

要素を探す
// CSS セレクタ — 1個
const title = document.querySelector('#title');
const firstP = document.querySelector('p');
const greenBtn = document.querySelector('.btn.green');

// CSS セレクタ — 複数 (NodeList)
const allP = document.querySelectorAll('p');
const items = document.querySelectorAll('.item');

CSSセレクタをそのまま使えるので直感的です。昔は getElementByIdgetElementsByClassNamegetElementsByTagName が別々にありましたが、モダンなコードではほぼquerySelectorだけ使います

querySelectorAll の NodeList — 配列ではない #

NodeList は配列ではない
const items = document.querySelectorAll('.item');

items.length;       // OK — 長さあり
items.forEach(...);  // OK — forEach 可能

items.map(...);      // ✗ map なし
items.filter(...);   // ✗ filter なし

// 本物の配列にするには
const arr = [...items];           // OK — spread
const arr2 = Array.from(items);   // OK

forEach はNodeList自体にありますが、map/filter/reduce はありません。配列メソッドが必要ならspreadまたは Array.from で変換します。

親/子/兄弟 — ツリー探索 #

ツリー探索
const el = document.querySelector('.item');

// 親
el.parentElement;
el.parentNode;       // ほぼ同じ (drift ケースあり)

// 子
el.children;          // HTMLCollection — 要素のみ
el.childNodes;        // NodeList — テキスト/コメント含む
el.firstElementChild;
el.lastElementChild;

// 兄弟
el.nextElementSibling;
el.previousElementSibling;

// 最も近い祖先 (自分を含む)
el.closest('.container');

closest はよく使います — イベント委譲 (#2) パターンの核心です。

内容を変える — textContent vs innerHTML #

テキスト vs HTML
const el = document.querySelector('#title');

// テキストのみ — 安全
el.textContent = '新しいタイトル';

// HTML パース — 危険な可能性
el.innerHTML = '<strong>新しいタイトル</strong>';

textContent はテキストとしてのみ入ります。ユーザー入力や外部データを入れても安全です。

innerHTML はHTMLとしてパースされます。<strong> タグが本物の強調としてレンダリングされますが、信頼できないデータを入れるとXSS攻撃に脆弱になります。

XSS 危険
const userInput = '<img src=x onerror="alert(1)">';

el.textContent = userInput;   // 安全 — ただの文字列
el.innerHTML = userInput;     // ✗ スクリプトが実行される

基本は textContent。HTMLが本当に必要なときだけ、その場で innerHTML を慎重に使ってください。

属性を扱う #

一般属性 #

属性
const link = document.querySelector('a');

link.href;                          // 直接プロパティでアクセス
link.href = 'https://...';
link.getAttribute('href');           // 同じ
link.setAttribute('href', 'https://...');
link.removeAttribute('href');

link.hasAttribute('target');         // true/false

hrefsrcid のようなありふれた属性は直接ドットアクセスのほうが短いです。よく知られていない属性や動的キーは getAttribute/setAttribute

データ属性 — data-* #

HTMLにJavaScript用のデータを埋め込むときに活用します。

HTML
<button data-id="42" data-action="delete">削除</button>
dataset でアクセス
const btn = document.querySelector('button');

btn.dataset.id;        // '42' (常に string)
btn.dataset.action;    // 'delete'

btn.dataset.id = '100';   // 属性更新 → HTML も変わる

camelCase変換に注意 — HTMLの data-user-name はJSで dataset.userName としてアクセスします。

クラスを扱う — classList #

classList
const el = document.querySelector('.box');

el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('open');                 // あれば外し、なければ追加
el.classList.toggle('disabled', isDisabled); // 第二引数で強制

el.classList.contains('active');   // boolean
el.classList.replace('a', 'b');

classList はモダンなJavaScriptの標準ツールです。昔の el.className = 'a b c' (文字列で直接扱う) よりも安全で可読性も良いです。

スタイルを扱う #

インラインスタイル #

style 属性
const el = document.querySelector('.box');

el.style.color = 'red';
el.style.backgroundColor = '#eee';   // CSS の background-color は JS で camelCase
el.style.transform = 'translateX(10px)';

ただしインラインスタイルは乱用しないでください。CSSクラスをトグルするほうがほぼ常にすっきりします。

推奨 — クラストグル
el.classList.add('error');
// CSS で .error { color: red; ... } を定義

CSS 変数を扱う #

CSSカスタムプロパティ (--var) は setProperty で動的に変えられます。

CSS 変数
el.style.setProperty('--main-color', '#ff5733');
const value = getComputedStyle(el).getPropertyValue('--main-color');

テーマ変更や動的スタイリングの場面に向いています。

エレメントを作って差し込む #

新しいエレメントを作る #

createElement
const div = document.createElement('div');
div.className = 'item';
div.textContent = '新しい項目';

差し込む — append / prepend / before / after #

差し込みメソッド
const parent = document.querySelector('.list');
const item = document.createElement('li');

parent.append(item);                 // 末尾に追加
parent.prepend(item);                // 先頭に追加
existingChild.before(item);          // 既存の子の前に
existingChild.after(item);           // 既存の子の後ろに
existingChild.replaceWith(item);     // 子を置き換え

古いメソッド (appendChildinsertBefore) も動作しますが — 新しいメソッドのほうが短く直感的なので、モダンなコードではこちらを使います。

一度に複数のエレメント #

複数の子を一度に
parent.append(li1, li2, li3);
parent.append('テキストも可能', divEl, '\n');

文字列も一緒に受け取って自動的にテキストノードに変換してくれます。

削除する — remove #

自分自身を削除
el.remove();

古いコードの el.parentNode.removeChild(el) よりずっと短いです。

効率 — DocumentFragment #

複数のエレメントを一度に追加するとき、ツリーに挿入するたびにreflowが起こると高くつきます。DocumentFragment が一時的なコンテナの役割をしてくれます。

複数の子を一括追加
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.append(li);   // fragment にいったん集める
}

list.append(fragment);   // 一度だけ本物のツリーに挿入 — reflow 1回

大きな差は100個程度から見えます。ほとんどの場面では意識する必要がありませんが、大きなリストを描くときに知っておくと良いです。

innerHTML + テンプレートリテラル — 短いが慎重に #

innerHTML で一度に
list.innerHTML = items
  .map((item) => `<li class="item">${item.name}</li>`)
  .join('');

これは非常に短いですが — ${item.name} がユーザー入力ならXSSリスクがあります。信頼できるデータでのみ、またはescape処理を経た後で使ってください。

エレメント情報 #

よく使う情報
el.tagName;              // 'DIV' (大文字)
el.id;
el.className;            // 文字列 — 推奨は classList

el.getBoundingClientRect();   // 画面上の位置/サイズ
el.offsetWidth;
el.offsetHeight;
el.offsetTop;
el.offsetLeft;

el.scrollTop;
el.scrollLeft;

レイアウトやスクロールを扱う場面でよく出会います。

closest の活用 #

上のツリー探索で少し見たメソッド。これがイベントハンドリングで非常によく使われるので、もう一度整理。

closest パターン
list.addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (!item) return;

  const id = item.dataset.id;
  // ...
});

リスト内のどこをクリックしても、最も近い .item を見つけて id を取り出します。次回の記事で本格的に扱うイベント委譲パターンの基盤です。

まとめ #

今回の記事で整理した内容。

  • DOMはHTMLをオブジェクトツリーにしたJavaScriptのインターフェース
  • querySelector / querySelectorAll がモダンな標準
  • NodeListは配列ではない — 配列メソッドが必要ならspread/Array.from
  • textContent が安全、innerHTML は信頼できるデータでのみ
  • datasetdata-* 属性を扱う
  • classList.add/remove/toggle がクラスの標準
  • append/prepend/before/after がモダンな差し込み
  • 大きなリストは DocumentFragment で一度に
  • closest で最も近い祖先を見つける

次回の記事 (#2 イベントハンドリングと委譲) では、イベントオブジェクト、バブリングとキャプチャリング、そして効率的なイベント委譲パターンを扱います。

X