Why doesn't nodelist have forEach?
NodeList now has forEach() in all major browsers
See nodeList forEach() on MDN.
Original answer
None of these answers explain why NodeList doesn't inherit from Array, thus allowing it to have forEach
and all the rest.
The answer is found on this es-discuss thread. In short, it breaks the web:
The problem was code that incorrectly assumed instanceof to mean that the instance was an Array in combination with Array.prototype.concat.That is, some code did something likeThere was a bug in Google's Closure Library which caused almost all Google's apps to fail due to this. The library was updated as soon as this was found but there might still be code out there that makes the same incorrect assumption in combination with concat.
if (x instanceof Array) {
otherArray.concat(x);
} else {
doSomethingElseWith(x);
}
However, concat
will treat "real" arrays (not instanceof Array) differently from other objects:[1, 2, 3].concat([4, 5, 6]) // [1, 2, 3, 4, 5, 6]
[1, 2, 3].concat(4) // [1, 2, 3, 4]
so that means that the above code broke when x
was a NodeList, because before it went down the doSomethingElseWith(x)
path, whereas afterward it went down the otherArray.concat(x)
path, which did something weird since x
wasn't a real array.For some time there was a proposal for an Elements
class that was a real subclass of Array, and would be used as "the new NodeList". However, that was removed from the DOM Standard, at least for now, since it wasn't feasible to implement yet for a variety of technical and specification-related reasons.
Why can I forEach through some NodeList's but not all?
It's because querySelectorAll
returns a NodeList
, but getElementsByClassName
returns a HTMLCollection
:
let querySelector = document.querySelectorAll(".text");let className = document.getElementsByClassName("text");console.log(querySelector.constructor.name);console.log(className.constructor.name);
<div class="text">Text</div><div class="text">Text</div><div class="text">Text</div><div class="text">Text</div>
forEach does not access all elements in NodeList
This is because, at a lower level, the .forEach()
is still keeping a count and using an index of the elements. As you remove some, you are removing them from the beginning of the array/node list, and then the indexer becomes inaccurate because all the element indexes have to get shifted down by one. The element that used to be at index position 5, is now at index position 4, for example.
For something like this, it's always better to use a counting loop and remove elements starting from the last index and working backwards. This will maintain a proper count for the duration of the loop because the element to index mapping won't be modified.
Here's an example:
let grid = document.getElementById("myTable");document.querySelector("input").addEventListener("click", function(){ for(i = grid.childNodes.length-1; i > -1; i--) { grid.removeChild(grid.childNodes[i]); }});
<table id="myTable"> <tr> <td>Row 1</td> </tr> <tr> <td>Row 2</td> </tr> <tr> <td>Row 3</td> </tr> <tr> <td>Row 4</td> </tr> <tr> <td>Row 5</td> </tr> </table>
<input type="button" value="Delete Rows">
.forEach() on Arrays vs NodeList (JavaScript)
Look
Things likeNodeLists and Arrays are two different things because NodeLists are
actually not a JavaScript API, but a browser API.
querySelectorAll()
and getElementsByTagName()
aren’t JavaScript methods, they’re browser APIs that let you access DOM elements. You can then manipulate them with JavaScript.NodeLists differ from Arrays in another meaningful way, too.
They are often live lists, meaning that if elements are removed or added to the DOM, the list updates automatically. querySelector()
and querySelectorAll()
return a static list (one that doesn’t update), but properties like .childNodes are live lists that will change as you manipulate the DOM (which can be a good or bad thing, depending on how you’re using it).
This is all made more confusing because arrays can contain nodes. And, there’s another, older type of list called an HTMLCollection that predates NodeLists, but is functionally similar (another article for another day).
They each have their own methods and properties, and you can convert a NodeList into an Array if you need to (but not the other way around).The key way to think about NodeLists vs. Arrays: NodeLists are a
language-agnostic way to access DOM elements, and Arrays are a
JavaScript object you can use to contain collections of stuff.
Why it is not possible to call forEach on a nodeList?
This is a fundamental thing in JavaScript: you can take a function from one object and apply to any other object. That is: call it with this
set to the object you apply the function to. It is possible, because in JavaScript all property names etc. are (plainly speaking) identified by name. So despite NodeList.length
being something different then Array.length
the function Array.forEach
can be applied to anything that exposes property length
(and other stuff that forEach
requires).
So what happens in your case is that:
querySelectorAll()
returns an object of type NodeList, which happens to exposelength
property and is enumerable (let's say it is accessible by[]
operator); NodeList does not exposeforEach
function (as you can see i.e here: https://developer.mozilla.org/en-US/docs/Web/API/NodeList) - that's why it's impossible to callforEach
directly on the results ofquerySelectorAll()
[].forEach
returns a function - this a not so clever shortcut forArray.prototype.forEach
- with
[].forEach.call(array, …)
this function is applied onto an object referenced byarray
, an object of type NodeList (that isforEach
is invoked witharray
asthis
in function body, so when insideforEach
there isthis.length
it refers tolength
inarray
despitearray
being NodeList and not real Array) - this works, because
forEach
is using properties that Array and NodeList have in common; it would fail if, i.e.forEach
wanted to use some property that Array has, but NodeList has not
Why forEach does not exist on NodeListOf
There is no guarantee forEach
will exist on this type - it can, but not necessarily (e.g. in PhantomJS and IE), so TypeScript disallows it by default. In order to iterate over it you can use:
1) Array.from():
Array.from(checkboxes).forEach((el) => { /* do something */});
2) for-in:for (let i in checkboxes) {
if (checkboxes.hasOwnProperty(i)) {
console.log(checkboxes[i]);
}
}
Loop through NodeList: Array.prototype.forEach.call() vs Array.from().forEach
Array.prototype.forEach.call(nodeList, callback)
will apply the logic of forEach
on the node list. forEach
just have a for
loop in it that goes from index 0
to this.length
and calling a callback on each of the items. This method is calling forEach
passing the node list as its this
value, since node lists have similar properties of an array (length
and 0
, 1
, ...), everything works fine.
Array.from(nodeList).forEach(callback)
will create a new array from the node list, then use forEach
on that new array. This second method could be split into two self explanatory lines:
var newArray = Array.from(nodeList); // create a new array out of the node list
newArray.forEach(callback); // call forEach on the new array
The first approach is better because it doesn't create additional uneeded resources and it work on node lists directly. forEach on querySelectorAll not working in recent Microsoft browsers
Most DOM methods and collection properties aren't actually arrays, they're collections:
querySelectorAll
returns a staticNodeList
(a snapshot of matching elements as of when you call it).getElementsByTagName
,getElementsByTagNameNS
,getElementsByClassName
, and thechildren
property on aParentNode
(Elements are parent nodes) return liveHTMLCollection
instances (if you change the DOM, that change is reflected live in the collection).getElementsByName
returns a liveNodeList
(not a snapshot).
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>
javascript forEach on nodelist
Nope....and forEach applies on arrays only right?
Array.prototype.forEach
is intentionally generic, it can be applied to any object that is array-like. From the spec:The spec clearly lays out what properties and/or methods will be used during the processing ofNOTE2: The
forEach
function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method.
forEach
; as long as the object referenced via this
during the call has those, forEach
can be used on that object. That's why using forEach.call
like that works: The call
method on function objects (forEach
is a function object) calls the function using the first argument you give call
as this
during the call, and passing along the following arguments as the arguments to the original function. So Array.prototype.forEach.call(x, y)
calls forEach
with this
set to x
and with the first argument set to y
. forEach
doesn't care about the type of this
, just that it has the relevant properties and methods as described in the specification's algorithm for it.Most of the Array.prototype
methods are like that, and indeed many others on the other standard prototypes.
Side note: The
NodeList
returned by querySelectorAll
recently became iterable on modern browsers, whcih means: 1. It works with ES2015+'s for-of
, and 2. It has forEach
natively now. (On modern browsers.)
Related Topics
JavaScript Strings Outside of the Bmp
Detect 64-Bit or 32-Bit Windows from User Agent or JavaScript
Single Page Application: Advantages and Disadvantages
Do Browsers Parse JavaScript on Every Page Load
Get the Current Year in JavaScript
Viewing All the Timeouts/Intervals in JavaScript
How to Auto-Reload a Chrome Extension I'm Developing
How to Close a Window with JavaScript on Mozilla Firefox 3
Why Does JavaScript's Regex.Exec() Not Always Return the Same Value
Attaching Click Event to a Jquery Object Not Yet Added to the Dom
Get Selected Value/Text from Select on Change
Promises for Promises That Are Yet to Be Created Without Using the Deferred [Anti]Pattern
JavaScript Curry: What Are the Practical Applications
Resize Jqgrid When Browser Is Resized
JavaScript String and Number Conversion
What Causes the Error "Can't Execute Code from a Freed Script"