JavaScriptで子要素を取得しようとしたとき、children と childNodes のどちらを使えばいいのか、なぜ childNodes だと空白も取得されてしまうのかで詰まった経験はないでしょうか。
この記事では children・childNodes・querySelectorAll の根本的な違いを整理し、子要素のループ処理・フィルタリング・実用パターンまで体系的に解説します。
children と childNodes の根本的な違い
子要素を取得するプロパティは複数ありますが、まず最も重要な違いを押さえましょう。
| プロパティ | 返すもの | 型 | テキスト・コメントノード | 動的更新 |
|---|---|---|---|---|
children |
子の Element のみ | HTMLCollection | 含まない | あり(ライブ) |
childNodes |
子の 全ノード | NodeList | 含む | あり(ライブ) |
querySelectorAll |
CSSセレクタにマッチする要素 | NodeList | 含まない | なし(静的) |
children や childNodes はライブコレクションです。DOMに子要素を追加・削除するとコレクションが自動的に変化します。querySelectorAll は取得時点のスナップショットなので、その後のDOM変化は反映されません。ループ中にDOMを変更する場合は静的な querySelectorAll か Array.from() で配列に変換してから操作するのが安全です。<ul id="fruit-list"> <li class="item">りんご</li> <li class="item">みかん</li> <li class="item sold-out">バナナ</li> <!-- コメント --> </ul>
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(環境によって異なる)
HTMLにインデントや改行があると、タグとタグの間の空白・改行が「テキストノード」として存在します。
childNodes はこれらをすべて含むため、期待より件数が多くなります。通常は children を使い、テキストノードやコメントノードの操作が必要なときだけ childNodes を使うのが基本方針です。children で子要素を取得する
children はElement(タグ)だけを返すため、ほとんどのユースケースで最もシンプルに使えます。
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
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操作には向きませんが、テキストコンテンツの構造解析や特定ノードの操作が必要な場面で使います。
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;
}
}
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>
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(サブメニュー内も含む)
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'
);
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 はライブコレクションのため、ループ中に要素を削除するとインデックスがずれてスキップが起きます。
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 が正しく進まない
}
}
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)
children と childNodes のどちらを使えばいいですか?children を使います。HTMLのタグ(Element)だけを対象にするので、改行や空白のテキストノードを意識する必要がありません。childNodes が必要なのは、テキストノードやコメントノードを直接操作する特殊なケースのみです。children に forEach が使えないのはなぜですか?children が返す HTMLCollection は配列ではないため、forEach や map が直接使えません。Array.from(el.children) か [...el.children] で配列に変換してから使ってください。一方 for...of は HTMLCollection にそのまま使えます。querySelectorAll("li") のように :scope を付けないと子孫すべてにマッチします。直接の子のみ取得するには querySelectorAll(":scope > li") と書くか、シンプルに element.children を使うのが確実です。children はライブコレクションのため、ループ中に要素を削除すると長さが変わりインデックスがずれます。Array.from(el.children) で静的配列に変換してからループするか、逆順(length - 1 から 0)でループすると安全です。el.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 で兄弟要素を取得する方法で詳しく解説しています。また最初・最後の子要素の取得については要素の最初・最後の子要素を取得する方法もあわせて参照してください。