removing childNodes using node.childNodes.forEach
Asked Answered
F

2

5

Traditionally, a suggested way of removing a node's children in Javascript is to do something like this:

while(node.firstChild) {
    node.removeChild(node.firstChild);
}

Recently, I attempted to remove all of a node's children using the built in forEach() method:

node.childNodes.forEach(child => {
    node.removeChild(child);
}

This didn't function as I expected. Instead of removing all child nodes, the forEach stopped executing, leaving nodes leftover. What I ended up having to do was use Array.from:

Array.from(node.childNodes)

And then I could remove nodes with forEach. The reason I could not use the traditional method mentioned above is because for some reason, one child was always left behind, causing an infinite loop.

Why does the childNodes.forEach method not remove all the nodes as I thought it would? What am I misunderstanding?

Forelli answered 17/1, 2018 at 21:55 Comment(2)
Do you see the pattern in the result? jsfiddle.net/k3pjkp2a Think about what happens to the nodelist on each iteration.Rabblerouser
The term "built-in" refers to features of ECMAScript §4.2, anything supplied by the host is a host method. So the forEach method of NodeList instances isn't "built-in", it's a host method. ;-)Infelicity
L
20

node.childNodes is a live collection. As you remove items from it, the collection itself is modified (live while you're iterating). Trying to iterate it as you are, causes elements to be removed from the collection and moved down in the array-like structure while you're iterating it, causing you to miss nodes.

As an example, when you call removeChild() on the 2nd element in the collection, that element itself is then removed from the collection. That causes what was the 3rd element to be moved into the spot in the collection where the 2nd element was. Now, your loop moves on to the 3rd element in the collection. But, that will skip over the element that is now in the 2nd position causing you to never remove it.

That means the only safe way to iterate through the actual collection and remove things is with a backwards traversal because removing things form the end does not cause other elements to change their position in the collection. Removing items from the front (which is what you were doing) does cause items to move in the collection.

Array.from() converts the live collection to a static array where items are not removed from the array while deleting items from the DOM.

I have a personal rule of DOM development to NEVER use a live collection while I'm modifying the DOM in any way because the danger that the live collection gets modified while I'm trying to use it is too high. Array.from() is a very simple way to get a copy of a live collection that's static and is safe to work with, even as the DOM is being modified.


Another safe way to delete them all is with this backwards iteration because items are removed from the end of the live collection which doesn't cause any items to move in the collection that you haven't yet processed:

for (let i = node.childNodes.length - 1; i >= 0; i--) {
   node.removeChild(node.childNodes[i]);
}

But, I generally find this more cumbersome than just converting to a static array with Array.from() as you've already discovered.

Leghorn answered 17/1, 2018 at 22:3 Comment(6)
I understand, thank you for taking the time to explain.Forelli
@Forelli - What in the world possessed the designers of the DOM to even offer us live collections is a mystery to me. They cause way more trouble than any utility I could ever imagine. Fortunately, querySelectorAll() can nearly always be used instead and it returns a non-live structure.Leghorn
@jfriend00—only opinion of course, but it may be much more efficient to get a collection once and keep a reference, knowing that it will be updated based on whatever other code is doing to it. Otherwise, you have to get a new copy each time since it may have been modified since the last time (and likely browsers keep live collections anyway for their own purposes). But very little code seems to have been written to take advantage of this feature (possibly because most didn't know it existed until too late).Infelicity
@Infelicity - Yeah, but I've NEVER found a programming situation where that was required or even really useful probably because it's so fast to just query the DOM whenever you want the latest state. And the whole live collection things causes many, many people problems when it changes unintentionally as you're using it. I've never heard anyone gush as how well thought out the DOM API is. Instead, we mostly all use some sort of wrapper around it to normalize it into something easier to use.Leghorn
@jfriend00—the API was written to be language neutral at a time when no one could have envisioned how we'd be using the DOM API today. Libraries and frameworks are written for all languages, APIs, etc. I don't think they indicate a failure of the language/API, they're just tools to help with particular designs/implementations. At least the DOM API seems to be implementing the better features of various libraries, so rather than having to use a crystal ball, the authors can just implement what is known to be useful through usage and seeing what works and what doesn't. ;-)Infelicity
Well, I haven't done much DOM work in the last couple years, but the last time I did, jQuery (even with its warts) was a breath of fresh air compared to plain DOM programming and that's even before you had to deal with browser compatibility issues. The overall DOM API design represents a snapshot in time from a very long time ago. It shows its age. Anyway, we've drifted pretty far off-topic.Leghorn
U
3

node.childNodes is a live collection, so when you remove a child from node in the forEach you mess with the iterator.

https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes

Urson answered 17/1, 2018 at 22:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.