getElementsByClassNameでforEachがうまくいかなかった

前回の投稿(document.getElementsByClassNameでうまく取得できない…)でgetElementsByClassNameで取得した値を、forEachで回した時に、意図した値が返ってこない原因について、@cfm-art様に凄く分かりやすく教えていただいたので、忘れる前にメモ!!

@cfm-art様ありがとうございます:sparkles:


getElementsByClassNameで意図しない値が返ってきた


script.js

MODAL.methods = {

show: function(elm){
var $self = elm;
var $next = $self.nextElementSibling;
if($next.classList.contains(MODAL.tar) && !$self.classList.contains(MODAL.active)) {
$self.classList.add(MODAL.active);
$next.classList.add(MODAL.active);

$next.style.display = 'block';

console.log(document.getElementsByClassName(MODAL.active));

MODAL.methods.overlay();
}
},
close: function(){
var $close = document.getElementById(MODAL.close);
console.log(document.getElementsByClassName(MODAL.active));
Array.prototype.forEach.call(document.getElementsByClassName(MODAL.active), function(elm){
elm.classList.remove(MODAL.active);
if(elm.classList.contains(MODAL.tar)){
elm.style.display = 'none';
}
});
$close.remove();
},
overlay: function(){
console.log(document.getElementsByClassName(MODAL.active));
document.body.insertAdjacentHTML('beforeend', '<div id="' + MODAL.close + '" class="modal-overlay" onClick="MODAL.methods.close();"><span class="modal-overlay_child">close</span></div>');
}
};



getElementsByClassNameで値を取得した際の各メソッドのlengthの返り値

※lengthの値が2で返ってきてほしい


  • MODAL.methods.show = length: 2

  • MODAL.methods.overlay = length: 2


  • MODAL.methods.close = length: 1 ←( ^ω^)・・・

MODAL.methods.closeだけlengthが1で返ってくる…

しばらく調べてみたものの理由が分からないのでQiitaで質問して、詳しく説明していただきました!


document.getElementsByClassNameの返却値は配列ではなく「HTMLCollection 」というものです。

これは大カッコを使って配列のようにアクセス出来る 配列様 オブジェクトですが、配列ではありません。

MDNには拙い翻訳で以下のように書かれています。

https://developer.mozilla.org/ja/docs/Web/API/HTMLCollection


HTML DOM内のHTMLCollection は生きて(live)います

それらは元になったdocumentが変更された時点で自動的に更新されます.


以下のようなコードを試してみるとその意味がよく分かるかと思います。

/*

<div class="some-class">要素1件目</div>
<div class="some-class">要素2件目</div>
のとき
*/

const list = document.getElementsByClassName('some-class');
console.log(list.length); // 2
list[0].classList.remove('some-class'); // 先頭の要素のclassを削除
console.log(list.length); // 1減って1に => getElementsByClassNameしたタイミングではなくリアルタイムのDOMの情報を見ている

つまり件のコードは実質、以下のようになっています。

const list = document.getElementsByClassName(MODAL.active);

for (let i = 0; i < list.length; ++i) {
list[i].classList.remove(MODAL.active);
}

処理の流れとしては

const list = document.getElementsByClassName(MODAL.active);

// for の中身
// forの1週目
let i = 0;
i < list.length; // 0 < 2
list[i].classList.remove(MODAL.active); // lengthが1減る
// forの2週目
++i; // 0 → 1
i < list.length; // 1 < 1 でループを抜ける

となっており、思った結果になっていないと推察できます。


原因は「HTMLCollection」が動的にclassを参照していたことが原因で、ループ中に参照しているclassを削除するとHTMLCollectionの返り値が変化してしまって意図しない結果になってました。


解決策

getElementsByClassNameをquerySelectorAllに変更!

HTMLCollectionを返すgetElementsByClassNameではなく、

静的なNodeListを返してくれるquerySelectorAllにすることで解決しました!


script.js

MODAL.methods = {

show: function(elm){
var $self = elm;
var $next = $self.nextElementSibling;
if($next.classList.contains(MODAL.tar) && !$self.classList.contains(MODAL.active)) {
$self.classList.add(MODAL.active);
$next.classList.add(MODAL.active);

$next.style.display = 'block';

console.log(document.getElementsByClassName(MODAL.active));

MODAL.methods.overlay();
}
},
close: function(){
var $close = document.getElementById(MODAL.close);
console.log(document.querySelectorAll('.' + MODAL.active));
Array.prototype.forEach.call(document.querySelectorAll('.' + MODAL.active), function(elm){
elm.classList.remove(MODAL.active);
if(elm.classList.contains(MODAL.tar)){
elm.style.display = 'none';
}
});
$close.remove();
},
overlay: function(){
console.log(document.getElementsByClassName(MODAL.active));
document.body.insertAdjacentHTML('beforeend', '<div id="' + MODAL.close + '" class="modal-overlay" onClick="MODAL.methods.close();"><span class="modal-overlay_child">close</span></div>');
}
};


querySelectorAllの返り値についてMDNにこう記載されています


document.querySelectorAll

https://developer.mozilla.org/ja/docs/Web/API/Document/querySelectorAll

返り値

指定されたセレクタの少なくとも 1 つにマッチする要素について 1 つの Element オブジェクトを含む、生きていない NodeList。

NodeList

https://developer.mozilla.org/ja/docs/Web/API/NodeList

対して、DOM への変更が NodeList オブジェクトの内容に影響を与えないものもあります。document.querySelectorAll() メソッドは、そのような静的な NodeList を返します。


※最後に

分からなかったとはいえ、Qiitaで質問してしまい本当に申し訳ないです…

以後気を付けます!

ご指摘ありがとうございました!