LoginSignup
19
14

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-31

前回の投稿(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で質問してしまい本当に申し訳ないです…
以後気を付けます!
ご指摘ありがとうございました!

19
14
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
14