188

I am Javascript beginner.

I am initing web page via the window.onload, I have to find bunch of elements by their class name (slide) and redistribute them into different nodes based on some logic. I have function Distribute(element) which takes an element as input and does the distribution. I want to do something like this (as outlined for example here or here):

var slides = getElementsByClassName("slide");
for(var i = 0; i < slides.length; i++)
{
   Distribute(slides[i]);
}

however this does not do the magic for me, because getElementsByClassName does not actually return array, but a NodeList, which is...

...this is my speculation...

...being changed inside function Distribute (the DOM tree is being changed inside this function, and cloning of certain nodes happen). For-each loop structure does not help either.

The variable slides act's really un-deterministicaly, through every iteration it changes its length and order of elements wildly.

What is the correct way to iterate through NodeList in my case? I was thinking about filling some temporary array, but am not sure how to do that...

EDIT:

important fact I forgot to mention is that there might be one slide inside another, this is actually what changes the slides variable as I have just found out thanks to user Alohci.

The solution for me was to clone each element into an array first and pass the array ono-by-one into Distribute() afterwards.

danronmoon
  • 3,814
  • 5
  • 34
  • 56
Kupto
  • 2,802
  • 2
  • 13
  • 16
  • 3
    This is actually the way to do it, so you must be messing something else up! – adeneo Apr 05 '13 at 21:09
  • the `Distribute()` function is to long and complex to be copied here, but I am certain that I am changing the DOM structure inside, I am also duplicating (cloning) elements there. When I debug it, I can see the variable `slides` changes every time it is passed inside. – Kupto Apr 05 '13 at 21:12
  • It does'nt change unless you actually change it somewhere. – adeneo Apr 05 '13 at 21:13
  • another funny thing is that everything works fine, if the iteration cycles the other way around ie. `for(var i = slides.length - 1; i > 0; i--)` – Kupto Apr 05 '13 at 21:16
  • 6
    I believe that `getElementsByClassName()` returns a live `nodeList`, so as elements with that class are added the length of the `nodeList` over which you're iterating changes. – David Thomas Apr 05 '13 at 21:20
  • @netrunner all my functions, variables, etc. I actually use in my HTML seam to comply with the conventions you posted. This is unfortunately probably not the reason of my problem... – Kupto Apr 05 '13 at 21:28
  • By the way, as a side note and [citing Crockford](http://javascript.crockford.com/code.html): _Most variables and functions should start with a lower case letter._ – excentris Apr 05 '13 at 21:29
  • @Kupto I just noted you mention you have a Distribute function, starting with a capital D, but you are right, this is not the reason of your problem :) – excentris Apr 05 '13 at 21:32
  • 3
    @Kupto- looping in reverse often solves this sort of issue, where the Distribute function removes or moves the element such that it no longer matches the getElementsByClassName call, for the reason that David Thomas gives. – Alohci Apr 05 '13 at 21:37
  • @Alohci yes you are right. =( I forgot to mention that there might be one `slide` inside another... – Kupto Apr 05 '13 at 21:41
  • Thanks guys to all of you. – Kupto Apr 05 '13 at 21:53

12 Answers12

227

According to MDN, the way to retrieve an item from a NodeList is:

nodeItem = nodeList.item(index)

Thus:

var slides = document.getElementsByClassName("slide");
for (var i = 0; i < slides.length; i++) {
   Distribute(slides.item(i));
}

I haven't tried this myself (the normal for loop has always worked for me), but give it a shot.

AutoBaker
  • 919
  • 2
  • 15
  • 31
Albert Xing
  • 5,620
  • 3
  • 21
  • 40
  • This is the right solution, unless you try to look up and change elements that have a same class and are within each other. I explained my workaround in edit to my question. – Kupto Apr 24 '13 at 12:56
  • Sure, didn't take that into account. – Albert Xing Apr 24 '13 at 16:25
  • Why is it this way, if I may ask? Why is it not implemented so that you are able to iterate over the nodes like this `for(var el in document.getElementsByClassName("foo")){}` ? – Nearoo Feb 05 '17 at 00:31
  • 3
    `for ... of` allows you to iterate over NodeList now as in `for (slide of slides) Distribute(slide)`. Browser support is patchy, but if you're transpiling then `for ... of` will be converted, but `NodeList.forEach` wouldn't. – Mr5o1 Oct 22 '17 at 01:16
148

If you use the new querySelectorAll you can call forEach directly.

document.querySelectorAll('.edit').forEach(function(button) {
    // Now do something with my button
});

Per the comment below. nodeLists do not have a forEach function.

If using this with babel you can add Array.from and it will convert non node lists to a forEach array. Array.from does not work natively in browsers below and including IE 11.

Array.from(document.querySelectorAll('.edit')).forEach(function(button) {
    // Now do something with my button
});

At our meetup last night I discovered another way to handle node lists not having forEach

[...document.querySelectorAll('.edit')].forEach(function(button) {
    // Now do something with my button
});

Browser Support for [...]

Showing as Node List

Showing as Node List

Showing as Array

Showing as Array

StudioTime
  • 22,603
  • 38
  • 120
  • 207
styks
  • 3,193
  • 1
  • 23
  • 36
  • 4
    Gotcha on this is that nodeLists don't have a forEach function on them in every browser. If you're willing to tinker with prototypes, it's simple enough to do: `if ( !NodeList.prototype.forEach ) {NodeList.prototype.forEach = Array.prototype.forEach;}` – joshcanhelp Jan 26 '17 at 01:43
  • Elegant solution if I combine your answer with the comment from @joshcanhelp. Thanks :) Of course this will only lead to a line advantage with multiple loops. – yarwest Feb 03 '17 at 21:43
  • 1
    You should avoid this because it might not work across all browsers. Here's a simple workaround that I use and seems to work perfectly everywhere: https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ – tixastronauta Feb 14 '17 at 11:25
  • I think you meant `[...document.getElementsByClassName('.edit')].forEach(function(button) {` – hostingutilities.com Mar 19 '19 at 00:19
  • @wp-overwatch.com the dot is not needed in classname. Correct version should be: `[...document.getElementsByClassName('edit')].forEach(function(button) {` – MXT Oct 30 '19 at 08:28
  • Could you please put in sample code where you have `// Now do something with my button`? I don't understand how to incorporate the `(button)` in `function(button)`. I just completely destroyed a website builder website due to a syntax error in my custom javascript & luckily somehow used Chrome's override feature to salvage. For context, I basically tried this: `document.querySelectorAll('.edit').forEach(function(button) { button.style.background-color = "transparent"; });` – velkoon Feb 18 '22 at 21:48
  • 1
    @velkoon - You need `button.style.backgroundColor`. Dashed properties are not allowed. – styks Feb 19 '22 at 01:11
30

An up-to-date answer in 2021

.getElementsBy* methods return a live HTMLCollection, not a NodeList, getElementsByName being an exception.

There are remarkable differencences between these two lists. Whereas HTMLCollection has two methods, NodeList has five methods, including NodeList.forEach, which can be used to iterate through a NodeList.

Live collections are problematic because there's no way to keep the collection updated under the hood. To achieve a reliable collection, the DOM is traversed every time a collection is accessed, in every current implementation of HTMLCollection. In practice this means, that every time you access a member of a live collection (including the length), the browser traverses the entire document to find the specific element.

The Standard says:

If a collection is live, then the attributes and methods on that object must operate on the actual underlying data, not a snapshot of the data.

Never iterate live HTMLCollection!

Instead, convert the collection to array, and iterate that array. Or rather get the elements using .querySelectorAll, which gives you a static NodeList and a more flexible way to select elements.

If you really need a live list of elements, use the closest possible common ancestor element as the context instead of document.

It's notable, that also live NodeLists exist. Examples of live NodeLists are Node.childNodes and the return value of getElementsByName.

Teemu
  • 22,918
  • 7
  • 53
  • 106
  • So what you're saying is don't use for loop on getElementsBy*? – Nomentsoa Andrianjatovo Sep 02 '21 at 16:24
  • 1
    @Nomentsoa Yes, exactly, or any loop. – Teemu Sep 02 '21 at 18:44
  • I don't think your comment about `getElementsBy*` returning `NodeList` circa 2013 is correct, do you have a citation for it? As far as I'm aware, they were always `HTMLCollection`s. For instance, even IE9 returned an `HTMLCollection` from `getElementsByTagName`. Similarly, a version of Firefox v17 I had lying around... – T.J. Crowder Mar 16 '22 at 10:16
  • @T.J.Crowder There was a short period of the time (I'd recall that was in 2013), when some browsers went back and fourth with HTMLCollection and NodeList (not sure if the standard was never changed). I'd recall you're a contributor at MDN, you can check the old docs from that time. – Teemu Mar 16 '22 at 10:19
  • It's definitely not correct as a blanket statement like the above. Just tried Firefox v12 (Firefox is a **lot** easier to get old copies of than Chrome) and again, `HTMLCollection`. (And if this were something that could easily be switched around, the standard wouldn't have two of them. :-) ) I'd just remove the claim unless you can provide a citation for it (and really, there's not much point with historical info like that anyway). – T.J. Crowder Mar 16 '22 at 10:27
  • @T.J.Crowder Well, if you think that improves the post, I'll remove it. Though in the question, there's also "_getElementsByClassName does not actually return array, but a NodeList_", which seems to be checked by OP, as there's even a link to NodeList. I'll try find some evidence for my statement at some point, I'll leave a comment here, if I can find any. – Teemu Mar 16 '22 at 10:36
  • @Teemu - That'd be interesting. Re the OP -- they may have been using a polyfill for `getElementsByClassName` (since not all browsers supported it) that used `querySelector` under the covers. (Whether or not they realized it or thought to mention it.) BTW, if you happen to know (or run across) a way to get old copies of Chrome, let me know if you would. :-) – T.J. Crowder Mar 16 '22 at 10:41
  • @T.J.Crowder The description of the behavior in the question refers to a live list ("_act's really un-deterministicaly, through every iteration it changes it's length and order of elements wildly_", the list being a HTMLCollection or a live NodeList is unknown. I'm not aware of where to get old Chrome copies, I put that on my "list" too. – Teemu Mar 16 '22 at 10:51
  • @Teemu - Ah, yes, quite so! So it won't be a polyfill using `querySelectorAll` (it wouldn't have been `querySelector` in any case, typo on my part!). – T.J. Crowder Mar 16 '22 at 10:55
  • @T.J.Crowder I kinda found an "evidence", not a documentation, but in [this answer](https://stackoverflow.com/a/15763722/1169519) there's an image of Chrome DevTools from year 2013. The listed object looks like a HTMLCollection, though, but in Chrome it was named as "NodeList". To add my original text back to the answer would need a link to the some docs from that time. Thanks for pointing this out. – Teemu Mar 16 '22 at 11:32
  • @T.J.Crowder In an old [whatwg standard](https://platform.html5.org/history/webapps/r228.html#the-document) the type of getElementsByClassName collection is mentioned as "NodeList". I'm not sure if that documentation is a bit too old, though. – Teemu Mar 16 '22 at 11:56
  • 1
    @Teemu - Interesting, thanks! Again, though, at least two of the major browsers at the time didn't do that, but perhaps Chrome did (I'd love to find out) and it was the (overwhelming) Google contingent in WHAT-WG who wrote that bit. :-) – T.J. Crowder Mar 16 '22 at 11:58
13

You could always use array methods:

var slides = getElementsByClassName("slide");
Array.prototype.forEach.call(slides, function(slide, index) {
    Distribute(slides.item(index));
});
Andrew
  • 131
  • 1
  • 3
11

Update 2022

Fastest and shortest solution

[...document.getElementsByClassName('className')].forEach(el => {
    // Do something with each element
})

Why does it work?

Iterating live HTML collection is extremely inefficient

As mentioned in styks' answer above, [...htmlCollection] converts the the class collection into an array. It is necessary to convert it to an array, since iterating a live HTMLCollection directly would be extremely inefficient. As teemu wrote above "Never iterate live HTMLCollection!":

Live collections are problematic because there's no way to keep the collection updated under the hood. To achieve a reliable collection, the DOM is traversed every time a collection is accessed, in every current implementation of HTMLCollection. In practice this means, that every time you access a member of a live collection (including the length), the browser traverses the entire document to find the specific element.

Why is it the fastest and most efficient?

Note that using [...arr] is the fastest and most efficient way to convert htmlCollection to an array. A performance comparison of all methods made by harpo can be found here: http://jsben.ch/h2IFA

(See all details about htmlCollection conversion to an array here)

Auc
  • 158
  • 1
  • 2
  • 10
lior bakalo
  • 440
  • 2
  • 9
7

I followed Alohci's recommendation of looping in reverse because it's a live nodeList. Here's what I did for those who are curious...

  var activeObjects = documents.getElementsByClassName('active'); // a live nodeList

  //Use a reverse-loop because the array is an active NodeList
  while(activeObjects.length > 0) {
    var lastElem = activePaths[activePaths.length-1]; //select the last element

    //Remove the 'active' class from the element.  
    //This will automatically update the nodeList's length too.
    var className = lastElem.getAttribute('class').replace('active','');
    lastElem.setAttribute('class', className);
  }
Community
  • 1
  • 1
ayjay
  • 1,272
  • 2
  • 16
  • 25
2

You could use Object.values + for...of loop:

const listA = document.getElementById('A');
const listB = document.getElementById('B');
const listC = document.getElementById('C');
const btn = document.getElementById('btn');

btn.addEventListener('click', e => {
  // Loop & manipulate live nodeLList
  for (const li of Object.values(listA.getElementsByClassName('li'))) {
    if (li.classList.contains('active')) {
      listB.append(li);
    } else {
      listC.append(li);
    }
  }
});
ul {
  display: inline-flex;
  flex-direction: column;
  border: 1px solid;
}

ul::before {
  content: attr(id);
}

.active {
  color: red;
}

.active::after {
  content: " (active)";
}
<ul id="A">
  <li class="li active">1. Item</li>
  <li class="li">2. Item</li>
  <li class="li">3. Item</li>
  <li class="li active">4. Item</li>
  <li class="li active">5. Item</li>
  <li class="li">6. Item</li>
  <li class="li active">7. Item</li>
  <li class="li">8. Item</li>
</ul>

<button id="btn">Distribute A</button>

<ul id="B"></ul>
<ul id="C"></ul>
One-liner:
Object.values(listA.getElementsByClassName('li')).forEach(li => (li.classList.contains('active') ? listB : listC).append(li))
Exodus 4D
  • 2,207
  • 2
  • 16
  • 19
  • why do you need `Object.values` here ? since it's already an iterable, wouldn't `for..of` suffice ? – kigiri Aug 25 '23 at 15:54
2

Here is the example I've done like this to assign random number to each class:

var a = $(".className").length;
for(var i = 0; i < a; i++){
    var val = $(".className")[i];
    var rand_ = (Math.random()*100).toFixed(0);
    $(val).val(rand_);
}

It's just for reference

1
 <!--something like this--> 
<html>
<body>



<!-- i've used for loop...this pointer takes current element to apply a 
 particular change on it ...other elements take change by else condition 
-->  


<div class="classname" onclick="myFunction(this);">first</div>  
<div class="classname" onclick="myFunction(this);">second</div>


<script>
function myFunction(p) {
 var x = document.getElementsByClassName("classname");
 var i;
 for (i = 0; i < x.length; i++) {
    if(x[i] == p)
    {
x[i].style.background="blue";
    }
    else{
x[i].style.background="red";
    }
}
}


</script>
<!--this script will only work for a class with onclick event but if u want 
to use all class of same name then u can use querySelectorAll() ...-->




var variable_name=document.querySelectorAll('.classname');
for(var i=0;i<variable_name.length;i++){
variable_name[i].(--your option--);
}



 <!--if u like to divide it on some logic apply it inside this for loop 
 using your nodelist-->

</body>
</html>
1

I had a similar issue with the iteration and I landed here. Maybe someone else is also doing the same mistake I did.

In my case, the selector was not the problem at all. The problem was that I had messed up the javascript code: I had a loop and a subloop. The subloop was also using i as a counter, instead of j, so because the subloop was overriding the value of i of the main loop, this one never got to the second iteration.

var dayContainers = document.getElementsByClassName('day-container');
for(var i = 0; i < dayContainers.length; i++) { //loop of length = 2
        var thisDayDiv = dayContainers[i];
        // do whatever

        var inputs = thisDayDiv.getElementsByTagName('input');

        for(var j = 0; j < inputs.length; j++) { //loop of length = 4
            var thisInput = inputs[j];
            // do whatever

        };

    };
J0ANMM
  • 7,849
  • 10
  • 56
  • 90
0

Simple and easy with Query Selector

const elements = document.querySelectorAll('.fixed-class-name');
for (let i = 0; i < elements.length; i++) {
  const element = elements[i];
  // Do something with each element
}
0

Nowdays you can just use for..of

for (const element of documents.getElementsByClassName('active')) {
  console.log(element)
}

If you do not need any array method like .filter / .map, i think it's the simpler option

kigiri
  • 2,952
  • 21
  • 23