Most DOM methods and collection properties aren't actually arrays, they're collections:
NodeList
only recently got forEach
(and keys
and a couple of other array methods). HTMLCollection
didn't and won't; it turned out adding them would break too much code on the web.
Both NodeList
and HTMLCollection
are iterable, though, meaning that you can loop through them with for-of
, expand them into an array via spread ([...theCollection]
), etc. But if you're running on a browser where NodeList
doesn't have forEach
, it's probably too old to have any ES2015+ features like for-of
or iteration.
Since NodeList
is specified to have forEach
, you can safely polyfill it, and it's really easy to do:
if (typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype.forEach) {
// Yes, there's really no need for `Object.defineProperty` here
NodeList.prototype.forEach = Array.prototype.forEach;
}
Direct assignment is fine in this case, because enumerable
, configurable
, and writable
should all be true
and it's a value property. (enumerable
being true
surprised me, but that's how it's defined natively on Chrome/Chromium/Edge/Etc., Firefox, the old Legacy Edge, and Safari).
In your own code, you can do that with HTMLCollection
as well if you want, just beware that if you're using some old DOM libs like MooTools or YUI or some such, they may be confused if you add forEach
to HTMLCollection
.
As I said before, NodeList
and HTMLCollection
are both specified to be iterable (because of this Web IDL rule¹). If you run into a browser that has ES2015+ features but doesn't make the collections iterable for some reason, you can polyfill that, too:
if (typeof Symbol !== "undefined" && Symbol.iterator && typeof NodeList !== "undefined" && NodeList.prototype && !NodeList.prototype[Symbol.iterator]) {
Object.defineProperty(NodeList.prototype, Symbol.iterator, {
value: Array.prototype[Symbol.iterator],
writable: true,
configurable: true
});
}
(And the same for HTMLCollection
.)
Here's a live example using both, try this on (for instance) IE11 (although it will only demonstrate forEach
), on which NodeList
doesn't have these features natively:
// Using only ES5 features so this runs on IE11
function log() {
if (typeof console !== "undefined" && console.log) {
console.log.apply(console, arguments);
}
}
if (typeof NodeList !== "undefined" && NodeList.prototype) {
// forEach
if (!NodeList.prototype.forEach) {
// Yes, there's really no need for `Object.defineProperty` here
console.log("Added forEach");
NodeList.prototype.forEach = Array.prototype.forEach;
}
// Iterability - won't happen on IE11 because it doesn't have Symbol
if (typeof Symbol !== "undefined" && Symbol.iterator && !NodeList.prototype[Symbol.iterator]) {
console.log("Added Symbol.iterator");
Object.defineProperty(NodeList.prototype, Symbol.iterator, {
value: Array.prototype[Symbol.iterator],
writable: true,
configurable: true
});
}
}
log("Testing forEach");
document.querySelectorAll(".container div").forEach(function(div) {
var html = div.innerHTML;
div.innerHTML = html[0].toUpperCase() + html.substring(1).toLowerCase();
});
// Iterable
if (typeof Symbol !== "undefined" && Symbol.iterator) {
// Using eval here to avoid causing syntax errors on IE11
log("Testing iterability");
eval(
'for (const div of document.querySelectorAll(".container div")) { ' +
' div.style.color = "blue"; ' +
'}'
);
}
<div class="container">
<div>one</div>
<div>two</div>
<div>three</div>
<div>four</div>
</div>
¹ It's confusing, because HTMLCollection
is iterable but it isn't marked with the iterable
declaration, which bizarrely in the JavaScript DOM bindings doesn't mean that something is iterable, it means that it has forEach
, entries
, keys
, values
, and it's iterable. But HTMLCollection
, which isn't marked with the iterable
declaration, is still iterable. Instead, it's iterable because of this Web IDL rule as mentioned earlier.