10

Let's say I have the following list:

  <ol>
    <li>Cookies <ol>
    <li>Coffee</li>
    <li>Milk</li>
    <li class="test1">Chocolate </li>
   </ol>

and I perform this selection at the end of my html

 var nodes = document.querySelectorAll('li:first-of-type');

When I tried in Chrome nodes.forEach it gave me an error. When I looked at the value it looked like an array. I actually was able to navigate it using a regular for like:

for(var i=0;i<nodes.length;i++){
   nodes[i].onclick= function(){ alert('Hello!'); };
} 

So, what is the actual returned type of document.querySelectorAll? why array methods did not work?

So, it looks like an array, can workaround it to make it work like an array but it is not an array?

Dalorzo
  • 19,834
  • 7
  • 55
  • 102

2 Answers2

12

The type of the result is a NodeList. Since it is an Array-like object, you can run the map, forEach and other Array.prototype functions on it like this:

var result = document.querySelectorAll('a');
Array.prototype.map.call(result, function(t){ return t; })

The map, forEach, any and other functions in the Array prototype work on Array-like objects. For example, let's define an object literal with numerical indexes (0,1) and a length property:

var arrayLike = { '0': 'a', '1': 'b', length: 2};

The forEach method, applied to the arrayLike object will like on a real Array.

Array.prototype.forEach.call(arrayLike, function(x){ console.log(x) } ); //prints a and b
Giovanni Filardo
  • 1,012
  • 8
  • 14
  • 1
    Using `call` and `apply`, you can pass the execution context as the first argument. – Giovanni Filardo Feb 11 '14 at 22:53
  • Thanks @GiovanniFilardo. What I am wondering now is why I can do it through Array.prototype.[method].call and yet the object itself is not an "array" – Dalorzo Feb 11 '14 at 23:00
  • Looks like an array, can work around it to make it work like an array but it is not an array? – Dalorzo Feb 11 '14 at 23:01
  • @Dalorzo I edited the answer to expand on the `call` and `apply` argument, because in the comment the code is not as readable. – Giovanni Filardo Feb 11 '14 at 23:08
  • Thank for the award :-) I realized just now what you meant by asking "can work around it to make it work like an array". The answer is yes/no. You could put the `forEach` or `map` function of Array.prototype in the prototype of your result. For example: var result = document.querySelectorAll('a'); result.constructor.prototype.forEach = Array.prototype.forEach; result.forEach(function(el){ console.log(el) }); This usually works almost exactly like you expect. I would not change the prototype of built-in objects, though. – Giovanni Filardo Feb 11 '14 at 23:26
6

In the documentation, it says that :

[the returned list of elements] is a non-live NodeList of element objects.

NodeList is different from an array (entirely-different prototype-chain) and the documentation also tells you why you cannot use forEach:

Why can't I use forEach or map on a NodeList?

NodeList are used very much like arrays and it would be tempting to use Array.prototype methods on them, however they don't have those methods.

JavaScript has an inheritance mechanism based on prototypes for both built–in objects (like Arrays) and host objects (like NodeLists). Array instances inherit array methods (such as forEach or map) because their prototype chain looks like the following:

myArray --> Array.prototype --> Object.prototype --> null (The prototype chain of an object can be obtained by calling Object.getPrototypeOf several times.)

forEach, map and the likes are own properties of the Array.prototype object.

Unlike arrays, NodeList prototype chain looks like the following:

myNodeList --> NodeList.prototype --> Object.prototype --> null

NodeList.prototype contains the item method, but none of the Array.prototype methods, so they cannot be used on NodeLists.

However, there are workarounds. Again from the documentation, you can use the Array.prototype.forEach and Array.prototype.map methods directly like so:

var forEach = Array.prototype.forEach;

var divs = document.getElementsByTagName( 'div' );
var firstDiv = divs[ 0 ];

forEach.call(firstDiv.childNodes, function( divChild ){
  divChild.parentNode.style.color = '#0F0';
});

UPDATE:

It seems NodeLists can now be accessed like an array* in this updated documentation:

Accessing the matches

Once the NodeList of matching elements is returned, you can examine it just like any array. If the array is empty (that is, its length property is 0), then no matches were found.

Otherwise, you can simply use standard array notation to access the contents of the list. You can use any common looping statement, such as:

var highlightedItems = userList.querySelectorAll(".highlighted");

highlightedItems.forEach(function(userItem) {
  deleteUser(userItem);
});

*While you can now use forEach, other methods like map are not available.

Community
  • 1
  • 1
Vivin Paliath
  • 94,126
  • 40
  • 223
  • 295
  • The type is `NodeList`. I'm not sure what you mean by "anything" separated by commas; perhaps it looks like that on the console, but that is simply the string representation of the `NodeList`; the resemblance is superficial. – Vivin Paliath Feb 11 '14 at 22:52
  • What I mean is what @Giovanni just posted I can use Array.prototype.forEach.call. This gives me the impression the "nodes" returned are of type array, isn't? – Dalorzo Feb 11 '14 at 22:55
  • No. The fact that you can use it with `Array.prototype.forEach` doesn't mean that the `NodeList` is an array. You can see that from the prototype chain. It just means that `NodeList` conforms to whatever interface that `forEach` requires to traverse through each element of a list. – Vivin Paliath Feb 11 '14 at 23:02
  • `arguments` is also array-like.. this article was helpful: http://www.nfriedly.com/techblog/2009/06/advanced-javascript-objects-arrays-and-array-like-objects/ – ptim May 01 '16 at 01:18