131

I have a Javascript function that accepts a list of HTML nodes, but it expects a Javascript array (it runs some Array methods on that) and I want to feed it the output of Document.getElementsByTagName that returns a DOM node list.

Initially I thought of using something simple like:

Array.prototype.slice.call(list,0)

And that works fine in all browsers, except of course Internet Explorer which returns the error "JScript object expected", as apparently the DOM node list returned by Document.getElement* methods is not a JScript object enough to be the target of a function call.

Caveats: I don't mind writing Internet Explorer specific code, but I'm not allowed to use any Javascript libraries such as JQuery because I'm writing a widget to be embedded into 3rd party web site, and I cannot load external libraries that will create conflict for the clients.

My last ditch effort is to iterate over the DOM node list and create an array myself, but is there a nicer way to do that?

Bharata
  • 13,509
  • 6
  • 36
  • 50
Guss
  • 30,470
  • 17
  • 104
  • 128
  • Better yet, create a function to convert from DOM node list, but that would really be my solution, I think you got it right. – Kristoffer Sall-Storgaard Apr 29 '10 at 06:12
  • > for (i=0;i<x.length;i++) Why get the length of the NodeList at every iteration? It's not only a waste of time, but since NodeLists are live collections, if anything in the body of the loop changes its length, you could loop endlessly or hit an index out-of-bounds. The latter is the worst that can happen if you assign the length to a variable, and an error is much better than an endless loop. –  Jul 29 '11 at 20:45
  • This is a really old question, but jQuery was built with the *.noConflict* method specifically so it would not cause conflict with other libraries (even itself), meaning that multiple versions of jQuery could be loaded on a page. That said, it's best to avoid using/loading a library unless you absolutely have to. – vol7ron Feb 26 '16 at 18:42
  • @vol7ron: fast-forward to 2016, and everyone is still uptight about the size that javascript libraries add to the page. Granted, JQuery minified and gzipped is 30KB, its still 30KB too much just to transform a node list :-) – Guss Mar 01 '16 at 00:23

7 Answers7

177

In es6 you can just use as follows:

  • Spread operator

     var elements = [... nodelist]
    
  • Using Array.from

     var elements = Array.from(nodelist)
    

more reference at https://developer.mozilla.org/en-US/docs/Web/API/NodeList

ρяσѕρєя K
  • 132,198
  • 53
  • 198
  • 213
camiloazula
  • 1,779
  • 2
  • 9
  • 2
  • 8
    so easy with `Array.from()` :D – JJJ Mar 09 '17 at 18:23
  • 5
    in case somebody is using this approach with Typescript (to ES5), only `Array.from` works, as TS transpiles this to `nodelist.slice` - which is not supported. – Peter Albert Mar 11 '17 at 20:44
  • I answered the same a **year** before you and you passed me on the votes? I cannot explain this.. – vsync Jun 07 '17 at 16:11
  • 3
    @vsync, your answer does not mention `Array.from` – ESR Jun 27 '17 at 00:42
  • @EdmundReed - so? how does that justifies it. it's longer to write so in real situtation it will never get to be used, only `spread` is used. – vsync Jun 27 '17 at 07:53
  • 2
    @vsync (tl;dr - not all of your audience are good programmers). I reckon it's down to how you formatted your answer: 1. you could have put your code sample in a block, rather than inline 2. writing "nodelist" instead of "document.querySelectorAll('p')" is less scary for some of the "programmers" on stack 3. Mentioning babel probably scared off some people as it wasn't strictly necessary to answer the question 4. Including the "for( p of ..." example probably confused some people as they didn't know how "of" works. 5. Link to MDN gives people a sense of authority (rightly or wrongly...) – goofballLogic Dec 01 '17 at 10:28
88

NodeLists are host objects, using the Array.prototype.slice method on host objects is not guaranteed to work, the ECMAScript Specification states:

Whether the slice function can be applied successfully to a host object is implementation-dependent.

I would recommend you to make a simple function to iterate over the NodeList and add each existing element to an array:

function toArray(obj) {
  var array = [];
  // iterate backwards ensuring that length is an UInt32
  for (var i = obj.length >>> 0; i--;) { 
    array[i] = obj[i];
  }
  return array;
}

UPDATE:

As other answers suggest, you can now can use in modern environments the spread syntax or the Array.from method:

const array = [ ...nodeList ] // or Array.from(nodeList)

But thinking about it, I guess the most common use case to convert a NodeList to an Array is to iterate over it, and now the NodeList.prototype object has the forEach method natively, so if you are on a modern environment you can use it directly, or have a pollyfill.

Christian C. Salvadó
  • 807,428
  • 183
  • 922
  • 838
  • 2
    This is creating an array with the original order of the list reversed, which I don't suppose is what the OP wants. Did you mean to do `array[i] = obj[i]` instead of `array.push(obj[i])`? – Tim Down Apr 29 '10 at 09:56
  • @Tim, right, I had it like that before but edited yesterday night without noticing it (3AM local time :), Thanks!. – Christian C. Salvadó Apr 29 '10 at 14:58
  • I was hoping for something else, but if the spec says its not standard then who am I to argue :-) – Guss Apr 29 '10 at 20:41
  • 9
    In what circumstances would `obj.length` be anything other then an integer value? – Peter May 29 '12 at 19:00
  • In Chrome, it turns out doing slice in code is actually faster than using the slice method from Array.prototype (12644 ns for a 103-object NodeList vs 16736 for Array.prototype.slice). Also, your backwards loop is slightly faster than a push loop (12982 for push vs 12644). – George Oct 01 '12 at 07:03
  • 1
    I can't believe it's that complicated. Ugly. That's a very common need in Web/JS programming. A new method for next release of language? – Andrew Koper May 31 '13 at 20:10
  • FWIW, you could use `~~obj.length` instead of `obj.length >>> 0` for a cast to int. – Tomalak May 16 '14 at 16:08
  • Is perhaps `for (var i = Math.max(obj.length >> 0, 0); i--;) {` a superior option so that an insanely long loop isn't executed if **for some reason** the length was a negative number? – Steven Lu Sep 17 '14 at 18:53
  • what is this `>>>` ? It does not seem to work, it always returns 0 for me. Is it a typo and should be just `>`? Never seen that triple operator before… – chitzui Aug 20 '17 at 07:47
  • You can't break out of a `forEach` loop on DOM lists. Arrays are wanted because they have convenient functions like `some` or `every` to break out easily. – The Coprolal Jul 09 '19 at 13:32
  • 1
    @AlbertoPerez, you're welcome!. Saludos hasta Madrid! – Christian C. Salvadó Jul 25 '19 at 13:25
19

Using spread (ES2015), it's as easy as: [...document.querySelectorAll('p')]

(optional: use Babel to transpile the above ES6 code to ES5 syntax)


Try it in your browser's console and see the magic:

for( links of [...document.links] )
  console.log(links);
vsync
  • 118,978
  • 58
  • 307
  • 400
  • At least at latest chrome, 44, I get this: Uncaught TypeError: document.querySelectorAll is not a function(…) – Nick Mar 14 '16 at 07:14
  • @OmidHezaveh - As I said, this is ES6 code. I don't know if Chrome 44 supports ES6 and if so, at what coverage. It's almost a year old browser and obviously you would have to run this code on a browser which supports ES6 spread. – vsync Mar 14 '16 at 10:30
  • Or transpile it to es5 before execution – HelloWorld Apr 02 '18 at 18:04
8

Use this simple trick

<Your array> = [].map.call(<Your dom array>, function(el) {
    return el;
})
Gena Shumilkin
  • 713
  • 4
  • 16
  • Can you please explain why do you think this has better chance of success than using `Array.prototype.slice` (or `[].slice` as you put it)? As a note, I'd like to comment that the IE specific error I documented in the Q happens in IE 8 or lower, where `map` is not implemented anyway. In IE 9 ("standards mode") or higher, both `slice` and `map` succeed in the same way. – Guss Dec 22 '15 at 08:59
8

Today, in 2018, we could use the ECMAScript 2015 (6th Edition) or ES6, but not all browsers can understand it (for ex. IE does not understand all of it). If you want you could use ES6 as follows: var array = [... NodeList]; (as spread operator) or var array = Array.from(NodeList);.

In other case (if you can not use ES6) you can use the shortest way to convert a NodeList to an Array:

var array = [].slice.call(NodeList, 0);.

For example:

var nodeList = document.querySelectorAll('input');
//we use "{}.toString.call(Object).slice(8, -1)" to find the class name of object
console.log({}.toString.call(nodeList).slice(8, -1)); //NodeList

var array = [].slice.call(nodeList, 0);
console.log({}.toString.call(array).slice(8, -1)); //Array

var result = array.filter(function(item){return item.value.length > 5});

for(var i in result)
  console.log(result[i].value); //credit, confidence
<input type="text" value="trust"><br><br>
<input type="text" value="credit"><br><br>
<input type="text" value="confidence">

But if you want to iterate over the DOM node list easy only, then you do not need to convert a NodeList to an Array. It's possible to loop over the items in a NodeList using:

var nodeList = document.querySelectorAll('input');
// Calling nodeList.item(i) isn't necessary in JavaScript
for(var i = 0; i < nodeList.length; i++)
    console.log(nodeList[i].value); //trust, credit, confidence
<input type="text" value="trust"><br><br>
<input type="text" value="credit"><br><br>
<input type="text" value="confidence">

Don't be tempted to use for...in or for each...in to enumerate the items in the list, since that will also enumerate the length and item properties of the NodeList and cause errors if your script assumes it only has to deal with element objects. Also, for..in is not guaranteed to visit the properties in any particular order. for...of loops will loop over NodeList objects correctly.

See too:

Bharata
  • 13,509
  • 6
  • 36
  • 50
6

While it is not really a proper shim, since there is no spec requiring working with DOM elements, I've made one to allow you to use slice() in this manner: https://gist.github.com/brettz9/6093105

UPDATE: When I raised this with the editor of the DOM4 spec (asking whether they might add their own restrictions to host objects (so that the spec would require implementers to properly convert these objects when used with array methods) beyond the ECMAScript spec which had allowed for implementation-independence), he replied that "Host objects are more or less obsolete per ES6 / IDL." I see per http://www.w3.org/TR/WebIDL/#es-array that specs can use this IDL to define "platform array objects" but http://www.w3.org/TR/domcore/ doesn't seem to be using the new IDL for HTMLCollection (though it looks like it might be doing so for Element.attributes though it only explicitly states it is using WebIDL for DOMString and DOMTimeStamp). I do see [ArrayClass] (which inherits from Array.prototype) is used for NodeList (and NamedNodeMap is now deprecated in favor of the only item that would still be using it, Element.attributes). In any case, it looks like it is to become standard. The ES6 Array.from might also be more convenient for such conversions than having to specify Array.prototype.slice and more semantically clear than [].slice() (and the shorter form, Array.slice() (an "array generic"), has, as far as I know, not become standard behavior).

Brett Zamir
  • 14,034
  • 6
  • 54
  • 77
4
var arr = new Array();
var x= ... get your nodes;

for (i=0;i<x.length;i++)
{
  if (x.item(i).nodeType==1)
  {
    arr.push(x.item(i));
  }
}

This should work, cross browser and get you all the "element" nodes.

Strelok
  • 50,229
  • 9
  • 102
  • 115
  • 1
    This is basically the same as @CMS's answer, except that it assumes I want only element nodes - which I don't. – Guss Apr 23 '12 at 10:29