How to convert a DOM node list to an array in Javascript?
Asked Answered
S

7

135

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?

Stackhouse answered 29/4, 2010 at 5:59 Comment(4)
Better yet, create a function to convert from DOM node list, but that would really be my solution, I think you got it right.Aegis
> 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.Flittermouse
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.Incoordinate
@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 :-)Stackhouse
U
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.

Unguiculate answered 29/4, 2010 at 6:18 Comment(11)
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])?Fontanel
@Tim, right, I had it like that before but edited yesterday night without noticing it (3AM local time :), Thanks!.Gyro
I was hoping for something else, but if the spec says its not standard then who am I to argue :-)Stackhouse
In what circumstances would obj.length be anything other then an integer value?Wooer
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).Shakeup
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?Remainder
FWIW, you could use ~~obj.length instead of obj.length >>> 0 for a cast to int.Heterogony
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?Genuflect
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…Seato
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.Thumping
@AlbertoPerez, you're welcome!. Saludos hasta Madrid!Gyro
S
188

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

Septemberseptembrist answered 18/5, 2016 at 10:45 Comment(6)
so easy with Array.from() :DDecant
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.Rhodium
I answered the same a year before you and you passed me on the votes? I cannot explain this..Freezedry
@vsync, your answer does not mention Array.fromAcentric
@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.Freezedry
@Freezedry (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...)Frontality
U
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.

Unguiculate answered 29/4, 2010 at 6:18 Comment(11)
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])?Fontanel
@Tim, right, I had it like that before but edited yesterday night without noticing it (3AM local time :), Thanks!.Gyro
I was hoping for something else, but if the spec says its not standard then who am I to argue :-)Stackhouse
In what circumstances would obj.length be anything other then an integer value?Wooer
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).Shakeup
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?Remainder
FWIW, you could use ~~obj.length instead of obj.length >>> 0 for a cast to int.Heterogony
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?Genuflect
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…Seato
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.Thumping
@AlbertoPerez, you're welcome!. Saludos hasta Madrid!Gyro
F
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);
Freezedry answered 13/12, 2015 at 11:5 Comment(3)
At least at latest chrome, 44, I get this: Uncaught TypeError: document.querySelectorAll is not a function(…)Ulster
@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.Freezedry
Or transpile it to es5 before executionLit
Q
9

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:

Quigley answered 2/7, 2018 at 14:47 Comment(0)
U
8

Use this simple trick

<Your array> = [].map.call(<Your dom array>, function(el) {
    return el;
})
Uppsala answered 22/12, 2015 at 8:14 Comment(1)
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.Stackhouse
G
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).

Guillermoguilloche answered 27/7, 2013 at 0:31 Comment(1)
I've updated to indicate that the specs may be moving in the direction of requiring this behavior.Guillermoguilloche
S
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.

Shortage answered 29/4, 2010 at 6:11 Comment(1)
This is basically the same as @CMS's answer, except that it assumes I want only element nodes - which I don't.Stackhouse

© 2022 - 2024 — McMap. All rights reserved.