The title of this post is what I originally googled. Here's what got me there:
I was working on displaying local times for the event listings on dev.to/events (haven't made a PR yet). To do this, I added a class to all elements with a timestamp, like this:
<span class="utc-time"><%= event.starts_at %></span>
I wanted to grab all the timestamps on the page, loop through them, and update their innerHTML
to reflect the local time. I usually use for
statements when I need to loop stuff, but I decided to try the .forEach
function.
var timestamps = document.getElementsByClassName("utc-time");
timestamps.forEach(function(timestamp) {
localTime = updateLocalTime(timestamps[i].innerHTML);
timestamps[i].innerHTML = localTime;
});
I got this error:
Eventually, I realized that timestamps
was not an array, it was a NodeList and at the top of mdn documentation, it clearly states:
Although NodeList is not an Array, it is possible to iterate on it using
forEach()
. It can also be converted to an Array usingArray.from()
.However some older browsers have not yet implemented
NodeList.forEach()
norArray.from()
. But those limitations can be circumvented by usingArray.prototype.forEach()
(more in this document).
I probably should have googled "How to loop through a NodeList" for specificity. Anyway, so then I wrote this:
Array.prototype.forEach.call(timestamps, function (timestamp) {
localTime = updateLocalTime(timestamp.innerHTML);
timestamp.innerHTML = localTime;
});
And it worked! But when I showed it to @maestromac, he told me that a simple for
statement would have worked. And would probably be a bit safer. So I went back to what I was most familiar with:
for (var i = 0; i < timestamps.length; i++) {
localTime = updateLocalTime(timestamps[i].innerHTML);
timestamps[i].innerHTML = localTime
}
At least I learned something about NodeLists today Β―_(γ)_/Β―
Top comments (25)
Ah this is a classic rites of passage in DOM unintutiveness.
If only
NodeList
inherited fromArray
...There are some methods that don't make much sense in relation to
NodeList
s. Mostly mutating methods likepush
,sort
orsplice
.The others are fine, though. I'd love to have
map
orfilter
πuse the "β¦" spread operator, which works for both
getElementsByClassName()
andquerySelectorAll()
so you can port easily later.I think you can also do:
Haven't tested it though π
Well, in the mdn documentation it had mentioned this:
So I assumed that if forEach wasn't supported, Array.from wouldn't be either?
I just tested it and it works on Chrome for me.
It might not be compatible with many browsers though, as you mentioned.
You don't have to go far - if you have Windows and IE11 installed, that doesn't support either π
The MDN documentation also has an Array.from polyfill:
developer.mozilla.org/en-US/docs/W...
A small correction: you used
document.getElementsByClassName
which does not return aNodeList
but aHTMLCollection
. Now, the former does haveforEach
defined - but it's pretty much the only array method that has been added to its prototype so far.But it's only a relatively recent addition, so older browsers don't support it - fortunately, the
Array#forEach
trick works pretty well, down to sufficiently old Internet Explorer versions (probably6? 5.5?The heck am I saying, that could work forslice
, butforEach
was added only in IE9...).A
HTMLCollection
is a totally different beast... and something that should be avoided in general. It's a live collection that gets updated when the DOM changes. Quite heavy when it comes to memory consumption and CPU usage.Conclusion: use
document.querySelectorAll
instead (which returns aNodeList
).I don't agree with that Β―\_(γ)_/Β―
It's true that every browser supports
for
(duh!), but experience proved that something that iterates over a collection for us is simpler as it doesn't force us to take care of a variable for counting, while the (relatively) complex - although well-known - syntax offor
is prone to mistakes.Mostly caused by distraction and/or boredom π
A best practice is to convert a
NodeList
to a normalArray
, so you can use the built-in forEach/map functions.One ES5 way is to create a helper function, for example on the NodeList prototype:
Another popular way is to create a jQuery-ish helper function that wraps the conversion to array. This is also used in Lea Verou's bliss.js library:
The modern ES7+ way is to use the
...
operator:I just want to make a few points.
for
loop in years.querySelectorAll
.getElementsByClassName
returns anHTMLCollection
, which is why you were getting the error.NodeList
s have aforEach
method andHTMLCollection
s do not.My initial instinct on circumventing this is the following:
HTMLCollection
s are array-like. They areiterable
and have alength
property. Theslice
function can convert array-like objects intoArray
s.this
is bound to our array-like object.slice
iterates overthis
using thelength
property since no other arguments were given. All the elements are returned in a newArray
then we can callforEach
method on ourArray
But looking at your initial answer:
Is a good solution too if not better. timestamps is bound to
this
thenforEach
method iterates over the array-like object by using thelength
property. My solution would have looped through the array-like object once then the newly created array versus this solution which is once.Yep. Wish we could use
forEach
on arrays. At the moment, I just convert them over to arrays by using the spread operator:At first, I would just use the
forEach
and continue to treat it as a node-list unless but I needed it in an array form, but Babel apparently doesn't convertforEach
to IE11 friendly syntax. So now, I just convert node-lists to arrays and then go on from there unless I add a specific IE11 polyfill.Wait, is this true?
I seldom use
selectElementsByClassName
, but according to mdn it returns aHTMLCollection
. And the mdn doc clearly says it hasn't gotforEach
.But for
NodeList
, you should be able to doforEach
. If you dodocument.querySelectorAll('.className')
, you will get anNodeList
and you should be able to doforEach
. See here.Howeverrrrr, since most older browsers won't have
NodeList.prototype.forEach
defined, it is probably safer to do what you suggestedArray.prototype.forEach.call(elements, ...)
or just[].forEach.call(elements, ...)
. A more "es6" way would probably beArray.from(elements).forEach(...)
.Orrrrr, you could do it with for-loops as you suggested. "es6" introduced this amazing
for-of
loop, it could loop through most list-like things. So the following would work as well.Of course, to safely use ES6 features you probably want your polyfills + babel set up to support old browsers.
Although it's a very good point that you can use a for-loop in this case, your own solution can be simplified to [].forEach.call, which would also work in case you'd need other array operations such as map, filter or reduce.