There is some very counterintuitive behaviour with HTMLCollection, even though it seems to be recommended to make a for loop, with an increasing number, to loop around it, when I tried to use it manipulating the elements it contains with the function appendChild to append them to some external element, the loop behaves in a very strange way which I cannot manage to explain.
Here is my example code:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="source">
<a src="" class="link">Link_0</a>
<a src="" class="link">Link_1</a>
<a src="" class="link">Link_2</a>
<a src="" class="link">Link_3</a>
<a src="" class="link">Link_4</a>
<a src="" class="link">Link_5</a>
<a src="" class="link">Link_6</a>
<a src="" class="link">Link_7</a>
<a src="" class="link">Link_8</a>
</div>
<div id="list">
</div>
<script type="text/javascript">
var links = document.getElementsByClassName('link')
var list = document.getElementById('list')
var linksArray = Array.from(links)
for (var i = 0; i < links.length; i++) {
let current = links[i]
// let current = linksArray[i]
let number = document.createElement('span')
number.innerHTML = '<br>' + i + ': innerText: ' + current.innerText + ' element: '
list.appendChild(number)
list.appendChild(current)
}
</script>
</body>
</html>
This is the output I get from this code:
Link_1 Link_3 Link_5 Link_7
0: innerText: Link_0 element: Link_0
1: innerText: Link_2 element:
2: innerText: Link_4 element: Link_4
3: innerText: Link_6 element:
4: innerText: Link_8 element: Link_8
5: innerText: Link_2 element:
6: innerText: Link_6 element: Link_6
7: innerText: Link_2 element:
8: innerText: Link_2 element: Link_2
It seems counterintuitive because I would expect it to be just an ordered list, but it turns out to be that. After some meditation I concluded it might be the fact that I am appending the elements, so if I cancel the part where I append each element, then it seems more correct the output:
Link_0 Link_1 Link_2 Link_3 Link_4 Link_5 Link_6 Link_7 Link_8
0: innerText: Link_0 element:
1: innerText: Link_1 element:
2: innerText: Link_2 element:
3: innerText: Link_3 element:
4: innerText: Link_4 element:
5: innerText: Link_5 element:
6: innerText: Link_6 element:
7: innerText: Link_7 element:
8: innerText: Link_8 element:
And, I came out with the solution: just turn the collection into an array. Doing it makes this output:
0: innerText: Link_0 element: Link_0
1: innerText: Link_1 element: Link_1
2: innerText: Link_2 element: Link_2
3: innerText: Link_3 element: Link_3
4: innerText: Link_4 element: Link_4
5: innerText: Link_5 element: Link_5
6: innerText: Link_6 element: Link_6
7: innerText: Link_7 element: Link_7
8: innerText: Link_8 element: Link_8
However, this does not seem to make sense for me still anyway, why would the documentation recommend to use this for loop for HTMLCollection (for example in https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection/length ), if the collection gets disturbed if you do anything with the element?
I would think that the fact that the collection is not a DOM element, and they remain appended to whatever they are appended when I make the collection, would imply that the collection should not be altered in case I change the parent node of one of the members of the collection. But it does not seem to happen that way.
On the other hand, it is also not clear for me why, even if the collection gets altered, there are some steps in which nothing is returned, or why does it return then the elements 0, 4, 8, 6, 2, in that order. Also no error was returned in the console log, even though I am apparently, given that elements seem to disappear from the collection after altering them, requesting an element that does not exist in the collection.
Could you please explain this strange behaviour?
I think this makes these collections somewhat useless for many applications, and it becomes then necessary to make the extra step of turning it into an array, for being able to actually use it.
getElementsByClassName
returns a live collection of the elements in the document matching the class, in tree order. If the tree gets rearranged, or elements with the class get put into a different position in the document tree (or removed), the collection will mutate itself at the moment that the position changed. Yes, it's quite unintuitive, which is why putting collections into a static and predictable array first can be a good idea.(Another approach that I prefer is to always use
querySelectorAll
, and to never usegetElementsByClassName
, becausequerySelectorAll
's NodeList is static)Here:
i = 0
:Link_0
is selected and put inside#list
(at the bottom of the tree). The first element matching.link
in tree order is nowLink_1
.i = 1
:Link_1
is the 0th index.Link_2
is the 1st index. SoLink_2
is selected and put inside#list
.i = 2
:Link_4
is selected and put inside#list
i = 3
:Link_6
is selected and put inside#list
i = 4
:Link_8
is selected and put inside#list
At this point, there is:
The process continues because we're only at
i = 5
, there are still more to go; now, the element at index 5 with the class in tree order isLink_0
. It gets selected and appended to#list
, moving it to the end, and the process repeats, eventually resulting in the strange ordering you're seeing.