【JavaScript】HTMLの子要素を取得する方法|children・childNodes・querySelectorAllの違いと使い分け・ループ・フィルタリングまで解説

JavaScriptで子要素を取得しようとしたとき、childrenchildNodes のどちらを使えばいいのか、なぜ childNodes だと空白も取得されてしまうのかで詰まった経験はないでしょうか。

この記事では childrenchildNodesquerySelectorAll の根本的な違いを整理し、子要素のループ処理・フィルタリング・実用パターンまで体系的に解説します。

スポンサーリンク

children と childNodes の根本的な違い

子要素を取得するプロパティは複数ありますが、まず最も重要な違いを押さえましょう。

プロパティ 返すもの テキスト・コメントノード 動的更新
children 子の Element のみ HTMLCollection 含まない あり(ライブ)
childNodes 子の 全ノード NodeList 含む あり(ライブ)
querySelectorAll CSSセレクタにマッチする要素 NodeList 含まない なし(静的)
「ライブ」と「静的」の違い:
childrenchildNodes はライブコレクションです。DOMに子要素を追加・削除するとコレクションが自動的に変化します。querySelectorAll は取得時点のスナップショットなので、その後のDOM変化は反映されません。ループ中にDOMを変更する場合は静的な querySelectorAllArray.from() で配列に変換してから操作するのが安全です。
確認用HTML
<ul id="fruit-list">
  <li class="item">りんご</li>
  <li class="item">みかん</li>
  <li class="item sold-out">バナナ</li>
  <!-- コメント -->
</ul>
children と childNodes の出力比較
const list = document.getElementById('fruit-list');

// children: li 要素のみ 3件
console.log(list.children);
// HTMLCollection(3) [li.item, li.item, li.item.sold-out]
console.log(list.children.length);  // 3

// childNodes: テキストノード(改行)・コメントノードも含む
console.log(list.childNodes);
// NodeList(9) [text, li.item, text, li.item, text, li.item.sold-out, text, comment, text]
console.log(list.childNodes.length); // 9(環境によって異なる)
childNodes が “多い” 理由:
HTMLにインデントや改行があると、タグとタグの間の空白・改行が「テキストノード」として存在します。childNodes はこれらをすべて含むため、期待より件数が多くなります。通常は children を使い、テキストノードやコメントノードの操作が必要なときだけ childNodes を使うのが基本方針です。

children で子要素を取得する

children はElement(タグ)だけを返すため、ほとんどのユースケースで最もシンプルに使えます。

children の基本
const list = document.getElementById('fruit-list');

// 全子要素を取得
const items = list.children;
console.log(items.length);  // 3

// インデックスでアクセス
console.log(items[0].textContent);  // "りんご"
console.log(items[1].textContent);  // "みかん"

// 最初・最後の子要素はプロパティでも取得できる
console.log(list.firstElementChild.textContent);  // "りんご"
console.log(list.lastElementChild.textContent);   // "バナナ"

// 子要素の数
console.log(list.childElementCount);  // 3
for…of でループする(推奨)
const list = document.getElementById('fruit-list');

// HTMLCollection は for...of でループできる
for (const item of list.children) {
  console.log(item.textContent);
}
// りんご
// みかん
// バナナ

// 配列メソッドを使うなら Array.from() で変換
const itemArray = Array.from(list.children);
itemArray.forEach((item, index) => {
  console.log(`${index + 1}番目: ${item.textContent}`);
});
スプレッド構文でも配列化できます:
const items = [...list.children];
これで map()filter()find() など配列メソッドが使えます。

childNodes でノード一覧を取得する

childNodes はテキストノードやコメントノードを含む全ノードを返します。通常のElement操作には向きませんが、テキストコンテンツの構造解析や特定ノードの操作が必要な場面で使います。

childNodes のノード種別を確認する
const list = document.getElementById('fruit-list');

for (const node of list.childNodes) {
  switch (node.nodeType) {
    case Node.ELEMENT_NODE:   // 1
      console.log('Element:', node.tagName, node.textContent.trim());
      break;
    case Node.TEXT_NODE:      // 3
      // 空白・改行のみならスキップ
      if (node.textContent.trim()) {
        console.log('Text:', node.textContent);
      }
      break;
    case Node.COMMENT_NODE:   // 8
      console.log('Comment:', node.textContent);
      break;
  }
}
childNodes からElement だけを取り出す
const list = document.getElementById('fruit-list');

// nodeType === 1 (ELEMENT_NODE) のみフィルタリング
const elements = [...list.childNodes].filter(
  node => node.nodeType === Node.ELEMENT_NODE
);
// → [li, li, li](children と同じ結果)

// ※ 通常は最初から list.children を使う方がシンプル

querySelectorAll で直接の子要素を取得する

querySelectorAll はCSSセレクタで柔軟に絞り込めます。直接の子要素のみ対象にするには :scope 疑似クラスを組み合わせます。

ネストしたリスト(深さのある構造)
<ul id="nav">
  <li class="menu-item">ホーム</li>
  <li class="menu-item has-sub">
    サービス
    <ul class="sub-menu">
      <li class="menu-item">デザイン</li>
      <li class="menu-item">開発</li>
    </ul>
  </li>
  <li class="menu-item">お問い合わせ</li>
</ul>
:scope で直接の子のみ取得
const nav = document.getElementById('nav');

// :scope > セレクタ で直接の子要素のみ
const directChildren = nav.querySelectorAll(':scope > li');
console.log(directChildren.length);  // 3(サブメニュー内のliは含まない)

// :scope なしだと子孫すべてにマッチしてしまう
const allItems = nav.querySelectorAll('li');
console.log(allItems.length);  // 5(サブメニュー内も含む)
children vs querySelectorAll の使い分け
const nav = document.getElementById('nav');

// children: 全直接子Elementをまとめて取得
const all = nav.children;                          // HTMLCollection(3)

// querySelectorAll: CSSセレクタで絞り込み
const active = nav.querySelectorAll(':scope > li.active');   // アクティブなもの
const hasSub = nav.querySelectorAll(':scope > li.has-sub');  // サブメニューあり

// 子要素の特定タグだけ
const inputs = form.querySelectorAll(':scope > input');

子要素をタグ名・クラス名・属性でフィルタリングする

取得した子要素を条件で絞り込む実用的なパターンです。

タグ名・クラス名・属性でフィルタリング
const list = document.getElementById('fruit-list');
const items = [...list.children];

// タグ名でフィルタ
const liOnly = items.filter(el => el.tagName === 'LI');

// クラス名でフィルタ(classList.contains が確実)
const soldOut = items.filter(el => el.classList.contains('sold-out'));
console.log(soldOut[0].textContent);  // "バナナ"

// 属性でフィルタ
const form = document.getElementById('myForm');
const requiredInputs = [...form.children].filter(
  el => el.hasAttribute('required')
);

// 特定の data 属性の値でフィルタ
const highlighted = items.filter(
  el => el.dataset.highlight === 'true'
);
querySelectorAll で直接フィルタリング(セレクタが明確なとき)
const list = document.getElementById('fruit-list');

// 1ステップで絞り込める(よりシンプル)
const soldOut  = list.querySelectorAll(':scope > li.sold-out');
const required = form.querySelectorAll(':scope > input[required]');
const checked  = form.querySelectorAll(':scope > input[type="checkbox"]:checked');

子要素をループしながら削除・追加するときの注意

children はライブコレクションのため、ループ中に要素を削除するとインデックスがずれてスキップが起きます。

NG:ライブコレクションをそのままループして削除
const list = document.getElementById('fruit-list');

// BAD: 削除すると children が動的に縮まり、i がずれる
for (let i = 0; i < list.children.length; i++) {
  if (list.children[i].classList.contains('sold-out')) {
    list.children[i].remove();  // 削除後、i が正しく進まない
  }
}
OK:Array.from() で静的配列化してから操作
const list = document.getElementById('fruit-list');

// GOOD: 配列に変換してからループ(静的スナップショット)
Array.from(list.children).forEach(item => {
  if (item.classList.contains('sold-out')) {
    item.remove();
  }
});

// または逆順ループ(インデックスがずれない)
for (let i = list.children.length - 1; i >= 0; i--) {
  if (list.children[i].classList.contains('sold-out')) {
    list.children[i].remove();
  }
}

実用パターン:子要素の検索・操作

子要素取得を応用した実用的なユーティリティです。

子要素ユーティリティ関数
/**
 * 直接の子要素の中から条件に合う要素を返す
 * @param {Element} parent
 * @param {string}  selector - CSSセレクタ
 * @returns {Element|null}
 */
function findChild(parent, selector) {
  return parent.querySelector(`:scope > ${selector}`);
}

/**
 * 直接の子要素の中から条件に合う全要素を返す
 * @param {Element} parent
 * @param {string}  selector - CSSセレクタ
 * @returns {Element[]}
 */
function findChildren(parent, selector) {
  return [...parent.querySelectorAll(`:scope > ${selector}`)];
}

/**
 * 子要素のインデックス(0始まり)を返す
 * @param {Element} child
 * @returns {number}
 */
function childIndex(child) {
  return [...child.parentElement.children].indexOf(child);
}

// 使用例
const nav = document.getElementById('nav');
const activeItem = findChild(nav, 'li.active');          // 最初の.activeなli
const allItems   = findChildren(nav, 'li:not(.hidden)'); // hidden以外のli
const clicked    = document.querySelector('.menu-item');
console.log(childIndex(clicked));  // 何番目の子か
子要素の入れ替え・並び替え
const list = document.getElementById('fruit-list');

// 先頭と末尾を入れ替え
const first = list.firstElementChild;
const last  = list.lastElementChild;
list.appendChild(first);   // firstを末尾に移動 → 自動的に元の位置から外れる

// 子要素をテキストのアルファベット順に並び替え
const sorted = [...list.children].sort((a, b) =>
  a.textContent.localeCompare(b.textContent, 'ja')
);
sorted.forEach(el => list.appendChild(el)); // 並び替え後の順序で再挿入

よくある質問(FAQ)

QchildrenchildNodes のどちらを使えばいいですか?
A通常は children を使います。HTMLのタグ(Element)だけを対象にするので、改行や空白のテキストノードを意識する必要がありません。childNodes が必要なのは、テキストノードやコメントノードを直接操作する特殊なケースのみです。
QchildrenforEach が使えないのはなぜですか?
Achildren が返す HTMLCollection は配列ではないため、forEachmap が直接使えません。Array.from(el.children)[...el.children] で配列に変換してから使ってください。一方 for...ofHTMLCollection にそのまま使えます。
Q直接の子要素だけを取得したいのに、孫要素まで取れてしまいます。
AquerySelectorAll("li") のように :scope を付けないと子孫すべてにマッチします。直接の子のみ取得するには querySelectorAll(":scope > li") と書くか、シンプルに element.children を使うのが確実です。
Qループ中に子要素を削除したら一部がスキップされました。
Achildren はライブコレクションのため、ループ中に要素を削除すると長さが変わりインデックスがずれます。Array.from(el.children) で静的配列に変換してからループするか、逆順(length - 1 から 0)でループすると安全です。
Q特定クラスを持つ子要素の数を数えるには?
Ael.querySelectorAll(":scope > .クラス名").length が最もシンプルです。または [...el.children].filter(c => c.classList.contains("クラス名")).length でも取得できます。

まとめ

目的 推奨する方法
直接の子Element全件取得 el.children
直接の子をCSSで絞り込み el.querySelectorAll(":scope > セレクタ")
子要素を配列メソッドで操作 [...el.children]Array.from(el.children)
子要素数を取得 el.childElementCount
テキスト・コメントノードも含む全ノード el.childNodes
ループ中にDOMを変更する Array.from() で静的化してからループ

子要素の取得と合わせて、CSSセレクタで要素を取得する方法はquerySelectorAll の使い方で、隣接する兄弟要素の取得はnextElementSibling で兄弟要素を取得する方法で詳しく解説しています。また最初・最後の子要素の取得については要素の最初・最後の子要素を取得する方法もあわせて参照してください。