querySelectorAll() はCSSセレクタの記法で複数の要素を取得できる便利なメソッドですが、対象の要素を動的に生成 or 削除する場合は記述の仕方で取得結果が異なります。
記述の違い
処理を直接記述するか、変数に格納して参照するかの違いです。
console.log(document.querySelectorAll('.card'));
const cards = document.querySelectorAll('.card');
console.log(cards);
取得結果はどう違ってくるのか
.card
クラスを持つ要素を動的に生成し、両者の取得結果を比較しました。
サンプルHTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>サンプル</title>
<style>
*{ box-sizing: border-box; }
.card{ width: 200px; height: 100px; padding: 5px 0 0 10px; margin-top: 10px; background-color:#dddddd; }
#card01{ background-color:#e02f2f; }
#card02{ background-color:#4973f2; }
#card03{ background-color:#f0e90c; }
#card04{ background-color:#48f026; }
.isSemitransparent{ opacity: 0.5; }
</style>
</head>
<body id="body">
<div id="cardWrap">
<div id="card01" class="card">カード1</div>
<div id="card02" class="card">カード2</div>
<div id="card03" class="card">カード3</div>
</div>
<script>
// ここに処理を書く
</script>
</body>
</html>
サンプルHTMLの初期表示。3枚のカードが表示されています。
コンソールで検証(4枚目の緑のカードを動的に生成)
処理を直接記述した場合の取得結果
最後に出力されたログで NodeList に生成した要素の情報が反映されています。
変数に格納して参照した場合の取得結果
最後に出力されたログの NodeList をご覧ください。カードは4枚表示されているのに、 NodeList は (3) のままです。生成した要素の情報が反映されていません。
変数の参照では動的に生成・削除された要素が反映されないのです。
なぜ取得結果に違いが出るのか
MDN(NodeList)に以下の説明がありました。
document.querySelectorAll() メソッドは、静的な NodeList を返します。
NodeList には静的・動的の概念が存在するようです。
静的・動的の違いはまだちゃんと理解できている自信がありませんが、色々調べたうえで下記のように解釈しました。
- 動的な NodeList ... Webページを構成するNodeを参照(シャローコピー)している NodeList
- 静的な NodeList ... Webページを構成するNodeを複製(ディープコピー)して作られた NodeList
※シャローコピー・ディープコピーという表現も、あくまでもイメージです
上記の解釈で考えると、変数に格納した静的な NodeList は処理が走った時点(変数に代入したタイミング)の Node を基に作られた複製となります。大元の Node と紐づいていないので、大元の Node に変化があっても影響を受けません。代入後に大元の Node が変化すると、変数に格納してある NodeList は実際の Node よりも古い情報になってしまいます。
動的に生成・削除した要素を反映したい
対象の要素が動的に生成・削除される状況 かつ それらの要素に対して何かを行いたいケースがあるかもしれません。
処理を直接記述する事で対応はできますが、 getElementsBy〜() メソッドを使用するのも一つの手です。
下記の前提で検証してみます。
- 対象の要素が動的に生成される
- 対象の要素はクリックすると半透明になる
scriptタグ内に以下のコードを記述します。
// getElementsByClassName()で取得したものを変数化
const cards = document.getElementsByClassName('card');
// cardクラスを持つ要素を生成
const cardWrap = document.getElementById('cardWrap');
const card04 = document.createElement('div');
card04.id = 'card04';
card04.classList = 'card';
card04.textContent = 'カード4';
cardWrap.appendChild(card04, cardWrap);
// for of文でcards内の要素全てにクリックイベントを与えてまわる
for (let item of cards) {
item.addEventListener('click', () => {
event.currentTarget.classList.add('isSemitransparent');
});
};
動的に生成した緑のカードにもクリックイベントが適用されます。
console.log で確認するとわかる事ですが、 getElementsByClassName() で要素を取得すると HTMLCollection が返ってきます。
HTMLCollection は動的で実際の Node の要素を反映するため、変数に格納して参照しても問題ありません。
ちなみに querySelectorAll() で同じ事をしようとすると、緑のカードだけクリックイベントが付きません。
まとめ
querySelectorAll() を使う時には、取得したい要素に動的な処理が加わることがないか注意しよう。
取得したい要素に動的な処理が加わる場合は、getElementsBy〜() を使うとよさそう。
余談
jQueryでは $() で要素を取得できますが、これも変数化すると querySelectorAll() と同じ問題が起こるので注意しましょう。
また、jQueryは$('.card').on('click', function(){ 〜 処理 〜 });
のような記述で.card
を持つ全ての要素にクリックイベントを適用できますが、querySelectorAll()やgetElementsBy〜()で同じ事をやろうとするとエラーを返されます。
const cards = document.querySelectorAll('.card');
cards.addEventListener('click', () => {
event.currentTarget.classList.add('isSemitransparent');
});
// => cards.addEventListener is not a function
const cards = document.getElementsByClassName('card');
cards.addEventListener('click', () => {
event.currentTarget.classList.add('isSemitransparent');
});
// => cards.addEventListener is not a function
イベント処理を加える addEventListener()メソッド は単体の要素に対して有効ですが、要素の集合に対しては使えません。jQueryの記述ではあたかも一括でイベント処理を適用しているように見えますが、裏側ではfor文による地道な反復処理が行われているようです。
参考:どうして!?document.querySelectorAll(selector).addEventListener()が動かないわけ