Why Doesn't Nodelist Have Foreach

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.

There 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.

That is, some code did something like

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

NodeLists and Arrays are two different things because NodeLists are
actually not a JavaScript API, but a browser API.

Things like 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).

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.

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).

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 expose length property and is enumerable (let's say it is accessible by [] operator); NodeList does not expose forEach 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 call forEach directly on the results of querySelectorAll()
  • [].forEach returns a function - this a not so clever shortcut for Array.prototype.forEach
  • with [].forEach.call(array, …) this function is applied onto an object referenced by array, an object of type NodeList (that is forEach is invoked with array as this in function body, so when inside forEach there is this.length it refers to length in array despite array 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 static NodeList (a snapshot of matching elements as of when you call it).
  • getElementsByTagName, getElementsByTagNameNS, getElementsByClassName, and the children property on a ParentNode (Elements are parent nodes) return live HTMLCollection instances (if you change the DOM, that change is reflected live in the collection).
  • getElementsByName returns a live NodeList (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

...and forEach applies on arrays only right?

Nope. Array.prototype.forEach is intentionally generic, it can be applied to any object that is array-like. From the spec:

NOTE2: 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.

The spec clearly lays out what properties and/or methods will be used during the processing of 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



Leave a reply



Submit