DEV Community

Julián Herrera
Julián Herrera

Posted on

Using A NodeList as an Array: A Practical Use for Object Composition

Using A NodeList as an Array

The Scenario:

I had queried a bunch of tags, using querySelectorAll(), and received a NodeList in return.

The Problem:

NodeLists are like arrays (i.e. they have a length property, they are accessed by an index in brackets: NodeList[0]) however, try using .map, or .filter, or .forEach on one.

The Approach:

The options out there were varied. From looping through and filling an Array to some more clever es6 options like:

var elements = [... nodelist]
Enter fullscreen mode Exit fullscreen mode
var elements = Array.from(nodelist)
Enter fullscreen mode Exit fullscreen mode

However, these have a problem... they worked too well. You now had an array INSTEAD of a NodeList. Sure, it had all the data from the NodeList but it no longer identified itself as a NodeList.
What's the problem with that?
Try:

nodeElementInTheArray.compareDocumentPosition(anotherNodeElementInTheArray)
Enter fullscreen mode Exit fullscreen mode

This will error out because the argument is not a true NodeListItem.

Lets reframe our needs

We don't need our NodeList to be an Array, we just need those properties from Arrays. This is the perfect place for object composition.

The Solution

Object.assign(*NODELIST*, Object.Array)
Enter fullscreen mode Exit fullscreen mode

Our NodeList remains a NodeList, and it acquires those Array traits we need while not modifying its prototype. I didn't see this solution anywhere, likely because it's never what is asked for. So instead of asking, how do I make A become B, ask how can I get A to behave like B, and the answer will likely be 'Object Composition'

Top comments (2)

Collapse
 
bcallaars profile image
Joseph Callaars • Edited

I like this solution. Only very recently I came across this problem myself as well (I mainly only do back end), and I solved it through [].forEach.call(NodeListHere, function () {}) as I didn't want to change original object.

Collapse
 
maxart2501 profile image
Massimo Artizzu

There are some issues in this article. I can guess some of those come from the notation you've used?

NodeLists are like arrays ... however, try using .map, or .filter, or .forEach on one.

That's correct for map and filter, but forEach has now been added to NodeList.prototype.

nodeElementInTheArray.compareDocumentPosition(anotherNodeElementInTheArray)

This will error out because the argument is not a true NodeListItem.

It won't. Elements in a NodeList are objects of class Node, and that's what matters, because compareDocumentPosition is defined in the prototype of the Node class. The class NodeListItem doesn't exist in JavaScript, and anyway spreading the node list into an array, or using Array.from on it, won't change the class of its items: they will still be Node instances.

The question remain, here: why do you need a NodeList for? In all my career, when I needed it, I've always been happy to have an array instead, back when in ES3 I had to do this:

var nodearray = Array.prototype.slice.call(nodelist);
Object.assign(*NODELIST*, Object.Array)

That won't work. First, it's just Array, while Object.Array doesn't exist. Then, as Array is just a constructor, it's the methods from Array.prototype that you'd probably want. But then again, Object.assign copies the properties from the source to the destination object, but only the enumerable ones. Now, map, filter and all the others are not enumerable, so your node list won't get any additional property.

Moreover, there's another concern here. Even if you do copy a method like map to your node list, like this:

nodelist.map = Array.prototype.map;
// or
nodelist.map = [].map;

The object returned by map would not be a NodeList, but an Array. (That makes a lot of sense, as you could get a list of whatever you want with map, but a NodeList is purely a list of... nodes.)

Other methods like push or splice modify the source object itself, but NodeList's are immutable in JavaScript so those methods would throw a TypeError.

One way to actually have the methods from arrays in a node list, and have it still as an instance of NodeList, is to replace its prototype:

nodelist.__proto__ = Array.prototype;

But this, of course, is not object composition, and it's also generally frowned upon as mutating an object's prototype has quite some overhead.