150

It bugs me that I can't just do document.querySelectorAll(...).map(...) even in Firefox 3.6, and I still can't find an answer, so I thought I'd cross-post on SO the question from this blog:

http://blowery.org/2008/08/29/yay-for-queryselectorall-boo-for-staticnodelist/

Does anyone know of a technical reason why you don't get an Array? Or why a StaticNodeList doesn't inherit from an Array in such a way that you could use map, concat, etc?

(BTW if it's just one function you want, you can do something like NodeList.prototype.map = Array.prototype.map;...but again, why is this functionality (intentionally?) blocked in the first place?)

1
  • 3
    Actually also getElementsByTagName does not return an Array, but a collection, and if you want to use it like an Array (with methods like concat etc.) you have to convert such collection into an Array by doing a loop and copy each element of the collection into an Array. Nobody ever complained about this. Commented Aug 16, 2010 at 12:02

7 Answers 7

314

You can use ES2015 (ES6) spread operator:

[...document.querySelectorAll('div')]

will convert StaticNodeList to Array of items.

Here is an example on how to use it.

[...document.querySelectorAll('div')].map(x => console.log(x.innerHTML))
<div>Text 1</div>
<div>Text 2</div>

3
  • 44
    Another way is to use Array.from(): Array.from(document.querySelectorAll('div')).map(x => console.log(x.innerHTML)) Commented Mar 12, 2018 at 22:03
  • 4
    works great, but this answer is a odd way to use map
    – quemeful
    Commented Oct 28, 2021 at 13:08
  • 11
    Also note that Array.from() takes a mapping function as its second parameter. Very useful to avoid a second iteration (it lets you map the array while it's still under construction, so there is just one iteration). Quoting from MDN: More clearly, Array.from(obj, mapFn, thisArg) has the same result as Array.from(obj).map(mapFn, thisArg), except that it does not create an intermediate array, and mapFn only receives two arguments (element, index) without the whole array, because the array is still under construction.
    – Simone
    Commented Oct 31, 2022 at 14:13
100

I believe it to be a philosophical decision of the W3C. The design of the W3C DOM [spec] is quite orthogonal to the design of JavaScript, as the DOM is meant to be platform and language neutral.

Decisions like "getElementsByFoo() returns an ordered NodeList" or "querySelectorAll() returns a StaticNodeList" are very much intentional, so that implementations don't have to worry about aligning their returned data structure based on language-dependent implementations (like .map being available on Arrays in JavaScript and Ruby, but not on Lists in C#).

The W3C aim low: they'll say a NodeList should contain a readonly .length property of type unsigned long because they believe every implementation can at least support that, but they won't say explicitly that the [] index operator should be overloaded to support getting positional elements, because they don't want to stymie some poor little language that comes along that wants to implement getElementsByFoo() but cannot support operator overloading. It's a prevalent philosophy present throughout much of the spec.

John Resig has voiced a similar option as yours, to which he adds:

My argument isn't so much that NodeIterator isn't very DOM-like it's that it isn't very JavaScript-like. It doesn't take advantage of the features present in the JavaScript language and use them to the best of its ability...

I do somewhat empathize. If the DOM was written specifically with JavaScript features in mind it would be a lot less awkward and more intuitive to use. At the same time I do understand the W3C's design decisions.

4
  • Thanks, that helps me make sense of the situation.
    – Kev
    Commented Apr 8, 2010 at 16:20
  • @Kev: I saw your comment on that blog article page questioning how you would go about converting the StaticNodeList to an array. I would endorse @mck89's answer as the way to go for converting a NodeList/StaticNodeList to a native Array, but that will fail in IE (8 obv) with a JScript error, since those objects are hosted/"special". Commented Apr 8, 2010 at 16:38
  • True, that's why I upvoted him. Someone else has cancelled my +1 though. What do you mean by hosted/special?
    – Kev
    Commented Apr 13, 2010 at 20:45
  • 1
    @Kev: hosted variables are any variables provided by the "host" environment (eg a web browser). For example document, window, etc. IE often implements these "specially" (for example as COM objects) that sometimes don't conform to normal usage, in small and subtle ways, such as Array.prototype.slice.call bombing when given a StaticNodeList ;) Commented Apr 23, 2010 at 13:55
43

I don't know why it returns a node list instead of an array, maybe because like getElementsByTagName it will update the result when you update the DOM. Anyway a very simple method to transform that result in a simple array is:

Array.prototype.slice.call(document.querySelectorAll(...));

and then you can do:

Array.prototype.slice.call(document.querySelectorAll(...)).map(...);
3
  • 3
    Actually it doesn't update the result when you update the DOM--hence 'static'. You have to manually call qSA again to update the result. +1 for the slice line though.
    – Kev
    Commented Apr 8, 2010 at 14:32
  • 2
    Yeah, like Kev said: qSA resultset is static, getElementsByTagName() resultset is dynamic.
    – joonas.fi
    Commented Jul 23, 2015 at 12:35
  • IE8 only supports querySelectorAll() in standards mode
    – mbokil
    Commented Jan 2, 2016 at 1:52
13

Just to add to what Crescent said,

if it's just one function you want, you can do something like NodeList.prototype.map = Array.prototype.map

Don't do this! It's not at all guaranteed to work.

No JavaScript or DOM/BOM standard specifies that the NodeList constructor-function even exists as a global/window property, or that the NodeList returned by querySelectorAll will inherit from it, or that its prototype is writable, or that the function Array.prototype.map will actually work on a NodeList.

A NodeList is allowed to be a ‘host object’ (and is one, in IE and some older browsers). The Array methods are defined as being allowed to operate on any JavaScript ‘native object’ that exposes numeric and length properties, but they're not required to work on host objects (and in IE, they don't).

It's annoying that you don't get all the array methods on DOM lists (all of them, not just StaticNodeList), but there's no reliable way round it. You'll have to convert every DOM list you get back to an Array manually:

Array.fromList= function(list) {
    var array= new Array(list.length);
    for (var i= 0, n= list.length; i<n; i++)
        array[i]= list[i];
    return array;
};

Array.fromList(element.childNodes).forEach(function() {
    ...
});
3
  • I agree +1. Just a comment, I think doing "var array = []" instead of "var array = new Array(list.length)" wouild make the code even shorter. But I'm interested if you know there might be a problem in doing this. Commented Aug 16, 2010 at 12:24
  • @MarcoDemaio: No, no problem. new Array(n) just gives the JS terp a hint on how long the array's going to end up. That could allow it to allocate that amount of space in advance, which would potentially result in a speedup as some memory reallocations could be avoided as the array grows. I don't know if it actually helps in modern browsers though... I would suspect not measurably.
    – bobince
    Commented Aug 16, 2010 at 12:56
  • 3
    Now it's implemented in Array.from() Commented Mar 12, 2018 at 22:00
13
Array.from(document.querySelectorAll(...)).map(...)

Not available on IE11 though https://caniuse.com/mdn-javascript_builtins_array_from

1
  • 1
    While this may not exactly answer the why question posed by OP ... it is still a valid, pragmatic, and concise solution to the problem the OP faces and worked well for me too. Many other posts talk about overriding the Element prototype which is dangerous. I would use this solution instead. Commented Jun 22, 2021 at 22:18
3

I think you can simply do following

Array.prototype.map.call(document.querySelectorAll(...), function(...){...});

It works perfect for me

0

This is an option I wanted to add to the range of other possibilities suggested by others here. It's meant for intellectual fun only and is not advised.


Just for the fun of it, here's a way to "force" querySelectorAll to kneel down and bow to you:

Element.prototype.querySelectorAll = (function(QSA){
    return function(){
        return [...QSA.call(this, arguments[0])]
    }
})(Element.prototype.querySelectorAll);

Now it feels good to step all over that function, showing it who's the boss. Now I don't know what's better, creating a whole new named function wrapper and then have all your code use that weird name (pretty much jQuery-style) or override the function like above once so the rest of your code would still be able to use the original DOM method name querySelectorAll.

  • Such approach would eliminate possible use of sub-methods

I wouldn't recommend this in any way, unless you honestly don't give a [you know what].

Not the answer you're looking for? Browse other questions tagged or ask your own question.