JavaScriptでDOM操作を学び始めると、かなり早い段階で次のようなコードに出会います。
const buttons = document.querySelectorAll(".accordion-button");
buttons.forEach(function (button) {
console.log(button);
});
このコードを見たとき、こう思いやすいです。
「querySelectorAll()で取得したものって、配列なの?」
結論から言うと、querySelectorAll()で取得できるものは、ArrayではなくNodeListです。
ただし、見た目はかなり配列に似ています。
この記事では、実際のHTMLを使いながら、ArrayとNodeListの違いを整理します。
まずHTMLを用意する
今回は、次のようなアコーディオン風のHTMLを例にします。
<div class="accordion">
<div class="accordion-item">
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">HTMLとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-content">
<p>HTMLは、Webページの構造を作るための言語です。</p>
</div>
</div>
<div class="accordion-item">
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">CSSとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-content">
<p>CSSは、Webページの見た目を整えるための言語です。</p>
</div>
</div>
<div class="accordion-item">
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">JavaScriptとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-content">
<p>JavaScriptは、Webページに動きをつけるための言語です。</p>
</div>
</div>
</div>
ここでは、.js-accordion-buttonというクラスが付いた要素が3つあります。
つまり、次の3つです。
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">HTMLとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">CSSとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">JavaScriptとは?</h2>
<span class="accordion-icon">+</span>
</div>
この3つをJavaScriptで取得してみます。
const buttons = document.querySelectorAll(".js-accordion-button");
console.log(buttons);
ブラウザのコンソールには、だいたい次のように表示されます。
NodeList(3) [
div.accordion-header.js-accordion-button,
div.accordion-header.js-accordion-button,
div.accordion-header.js-accordion-button
]
ここで重要なのは、表示がArrayではなくNodeListになっていることです。
NodeListとは何か
NodeListとは、DOMから取得した複数のノードの集まりです。
今回で言えば、.js-accordion-buttonに一致した3つのdiv要素の集まりです。
const buttons = document.querySelectorAll(".js-accordion-button");
このbuttonsの中には、次のような要素が順番に入っています。
buttons[0] // HTMLとは?のボタン
buttons[1] // CSSとは?のボタン
buttons[2] // JavaScriptとは?のボタン
実際に確認できます。
console.log(buttons[0]);
console.log(buttons[1]);
console.log(buttons[2]);
出力されるのは、それぞれのHTML要素です。
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">HTMLとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">CSSとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">JavaScriptとは?</h2>
<span class="accordion-icon">+</span>
</div>
このように、NodeListは複数のHTML要素を順番に持っています。
だから配列のように見えます。
しかし、配列そのものではありません。
Arrayとは何か
Arrayは、JavaScriptの配列です。
const numbers = [100, 200, 300];
このnumbersはArrayです。
console.log(numbers);
出力は次のようになります。
[100, 200, 300]
配列なので、map()やfilter()などの配列用メソッドを使えます。
const numbers = [100, 200, 300];
const doubledNumbers = numbers.map(function (number) {
return number * 2;
});
console.log(doubledNumbers);
出力は次のようになります。
[200, 400, 600]
Arrayは、JavaScriptがもともと持っている「配列」です。
一方、NodeListはDOM操作で出てくる「ノードのリスト」です。
ArrayとNodeListの違い
違いを一言で言うと、こうです。
ArrayはJavaScriptの配列です。
NodeListはDOMから取得した要素の集まりです。
見た目は似ていますが、正体が違います。
const numbers = [100, 200, 300];
console.log(numbers);
これはArrayです。
const buttons = document.querySelectorAll(".js-accordion-button");
console.log(buttons);
これはNodeListです。
どちらも次のように番号でアクセスできます。
numbers[0];
buttons[0];
どちらもlengthを使えます。
console.log(numbers.length);
console.log(buttons.length);
しかし、使えるメソッドが違います。
NodeListは配列ではないのでmapがそのまま使えない
たとえば、配列ならmap()が使えます。
const numbers = [100, 200, 300];
const result = numbers.map(function (number) {
return number * 2;
});
console.log(result);
これは問題なく動きます。
しかし、NodeListに対して同じ感覚でmap()を使うとエラーになります。
const buttons = document.querySelectorAll(".js-accordion-button");
const titles = buttons.map(function (button) {
return button.textContent;
});
console.log(titles);
これは動きません。
理由は、buttonsがArrayではなくNodeListだからです。
NodeListには、map()が用意されていません。
では、なぜNodeListでforEachは使えるのか
ここが初心者にとって一番混乱しやすいところです。
NodeListは配列ではありません。
しかし、最近のブラウザではNodeListにもforEach()があります。
そのため、次のコードは動きます。
const buttons = document.querySelectorAll(".js-accordion-button");
buttons.forEach(function (button) {
console.log(button);
});
ここでやっていることは、.js-accordion-buttonが付いた要素を1つずつ取り出しているだけです。
1回目のbuttonには、HTMLのボタンが入ります。
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">HTMLとは?</h2>
<span class="accordion-icon">+</span>
</div>
2回目のbuttonには、CSSのボタンが入ります。
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">CSSとは?</h2>
<span class="accordion-icon">+</span>
</div>
3回目のbuttonには、JavaScriptのボタンが入ります。
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">JavaScriptとは?</h2>
<span class="accordion-icon">+</span>
</div>
つまり、次のような処理をしているイメージです。
console.log(buttons[0]);
console.log(buttons[1]);
console.log(buttons[2]);
これをまとめて書けるのがforEach()です。
forEachでクリックイベントを付ける
実際のDOM操作では、取得した要素に対してクリックイベントを付けることが多いです。
const buttons = document.querySelectorAll(".js-accordion-button");
buttons.forEach(function (button) {
button.addEventListener("click", function () {
console.log("クリックされました");
});
});
このコードは、3つの.js-accordion-buttonすべてにクリックイベントを付けています。
つまり、実際には次のようなことをしています。
buttons[0].addEventListener("click", function () {
console.log("クリックされました");
});
buttons[1].addEventListener("click", function () {
console.log("クリックされました");
});
buttons[2].addEventListener("click", function () {
console.log("クリックされました");
});
ただし、毎回このように書くのは面倒です。
だからforEach()を使って、まとめて処理します。
NodeListをArrayに変換する
NodeListに対してmap()やfilter()を使いたい場合は、Arrayに変換します。
代表的な方法はArray.from()です。
const buttons = document.querySelectorAll(".js-accordion-button");
const buttonArray = Array.from(buttons);
console.log(buttonArray);
これでNodeListをArrayに変換できます。
変換後はmap()が使えます。
const buttons = document.querySelectorAll(".js-accordion-button");
const buttonArray = Array.from(buttons);
const titles = buttonArray.map(function (button) {
return button.textContent;
});
console.log(titles);
出力は次のようになります。
[
"HTMLとは?+",
"CSSとは?+",
"JavaScriptとは?+"
]
textContentは、その要素の中にある文字を取得します。
今回のbuttonの中にはh2とspanがあるので、タイトルと+の文字が取得されています。
スプレッド構文でもArrayに変換できる
Array.from()以外に、スプレッド構文でも変換できます。
const buttons = document.querySelectorAll(".js-accordion-button");
const buttonArray = [...buttons];
console.log(buttonArray);
これでもNodeListをArrayにできます。
その後は、配列としてmap()を使えます。
const titles = [...buttons].map(function (button) {
return button.textContent;
});
console.log(titles);
タイトルだけを取得したい場合
先ほどの例では、textContentで+まで取得されてしまいました。
タイトルだけを取得したい場合は、各ボタンの中にある.accordion-titleを取得します。
const buttons = document.querySelectorAll(".js-accordion-button");
const titles = [...buttons].map(function (button) {
const title = button.querySelector(".accordion-title");
return title.textContent;
});
console.log(titles);
出力は次のようになります。
[
"HTMLとは?",
"CSSとは?",
"JavaScriptとは?"
]
ここでは、まず.js-accordion-buttonを3つ取得しています。
そのあと、各ボタンの中から.accordion-titleを探しています。
const title = button.querySelector(".accordion-title");
つまり、全体から探すのではなく、それぞれのボタンの中からタイトルを探しているということです。
querySelectorAllはNodeListを返す
ここまでの話を整理すると、querySelectorAll()はNodeListを返します。
const buttons = document.querySelectorAll(".js-accordion-button");
このbuttonsは配列ではありません。
NodeListです。
だから、次のような理解になります。
const buttons = document.querySelectorAll(".js-accordion-button");
これは、.js-accordion-buttonが付いたHTML要素を全部探して、NodeListとして返している。
buttons.forEach(function (button) {
console.log(button);
});
これは、NodeListの中の要素を1つずつ取り出して処理している。
const buttonArray = Array.from(buttons);
これは、NodeListをArrayに変換している。
querySelectorとの違い
似たものにquerySelector()があります。
const button = document.querySelector(".js-accordion-button");
これは、最初に見つかった1つだけを取得します。
今回のHTMLでは、.js-accordion-buttonが3つあります。
しかし、querySelector()で取得できるのは最初の1つだけです。
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">HTMLとは?</h2>
<span class="accordion-icon">+</span>
</div>
一方、querySelectorAll()は一致するものを全部取得します。
const buttons = document.querySelectorAll(".js-accordion-button");
取得されるのは3つです。
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">HTMLとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">CSSとは?</h2>
<span class="accordion-icon">+</span>
</div>
<div class="accordion-header js-accordion-button">
<h2 class="accordion-title">JavaScriptとは?</h2>
<span class="accordion-icon">+</span>
</div>
つまり、querySelector()は1個、querySelectorAll()は全部です。
そして、querySelectorAll()で返ってくる全部の集まりがNodeListです。
まとめ
ArrayはJavaScriptの配列です。
NodeListはDOMから取得したノードの集まりです。
querySelectorAll()で取得したものは、ArrayではなくNodeListです。
NodeListは番号でアクセスできます。
buttons[0];
buttons[1];
buttons[2];
lengthも使えます。
buttons.length;
forEach()も使えます。
buttons.forEach(function (button) {
console.log(button);
});
しかし、map()やfilter()などの配列メソッドをそのまま全部使えるわけではありません。
配列として扱いたい場合は、Array.from()やスプレッド構文でArrayに変換します。
const buttonArray = Array.from(buttons);
const buttonArray = [...buttons];
初心者のうちは、次のように覚えると分かりやすいです。
Arrayは、JavaScriptの普通の配列。
NodeListは、HTMLから取ってきた要素のリスト。
NodeListは配列っぽいけど、配列そのものではない。
これが、ArrayとNodeListの一番大事な違いです。