Filter or map nodelists in ES6
Asked Answered
P

7

183

What is the most efficient way to filter or map a nodelist in ES6?

Based on my readings, I would use one of the following options:

[...nodelist].filter

or

Array.from(nodelist).filter

Which one would you recommend? And are there better ways, for example without involving arrays?

Polygamous answered 24/9, 2015 at 15:21 Comment(2)
Basically, both methods do the same thing. It you're using babel, then [...coll] will simply call Array.from(coll) for anything that is not an Array.Threatt
FWIW, ... syntax may be unsupported by older IDEs while Array.from() is just a regular method.Tibbetts
E
254
  • [...nodelist] will make an array of out of an object if the object is iterable.
  • Array.from(nodelist) will make an array out of an object if the object is iterable or if the object is array-like (has .length and numeric props)

Your two examples will be identical if NodeList.prototype[Symbol.iterator] exists, because both cases cover iterables. If your environment has not been configured such that NodeList is iterable however, your first example will fail, and the second will succeed. Babel currently does not handle this case properly.

So if your NodeList is iterable, it is really up to you which you use. I would likely choose on a case-by-case basis. One benefit of Array.from is that it takes a second argument of a mapping function, whereas the first [...iterable].map(item => item) would have to create a temporary array, Array.from(iterable, item => item) would not. If you are not mapping the list however, it does not matter.

Earshot answered 24/9, 2015 at 16:59 Comment(4)
Just note that Array.from isn't support on IE.Etka
@YannDìnendal That's true, but this question is asking about ES6, and IE doesn't support ES6 at all pretty much, so already a dead end.Earshot
Yes it's true. :) I should have clarified what I meant: if you rely on Babel to transpile es6 to es5, it will not by default polyfill prototype methods. So just something to be aware of. :)Etka
Is this better or worse than Array.prototype.map.call(nodelist, fn) ? (I'm curious if map.call is more performant, and "most efficient" is part of the question. Although I'm aware that 'efficien' is not the same as 'performant'.)Tammany
H
24

I found a reference that uses map directly on the NodeList by

Array.prototype.map.call(nodelist, fn)

I haven't tested it, but it seems plausible that this would be faster because it should access the NodeList directly.

Hindermost answered 16/4, 2018 at 4:50 Comment(1)
It is even easier then that: NodeList.prototype.map = Array.prototype.mapFumigate
C
21

TL;DR;

Array.prototype.slice.call(nodelist).filter

The slice() method returns an array. That returned array is a shallow copy of collection (NodeList) So it works faster than Array.from() So it works as fast as Array.from()

Elements of the original collection are copied into the returned array as follows:

  • For object references (and not the actual object), slice copies object references into the new array. Both the original and new array refer to the same object. If a referenced object changes, the changes are visible to both the new and original arrays.
  • For strings, numbers and booleans (not String, Number and Boolean objects), slice copies the values into the new array. Changes to the string, number or boolean in one array do not affect the other array.

Short explanation regarding arguments

Array.prototype.slice(beginIndex, endIndex)

  • takes optional args beginIndex and endIndex. If they are not provided slices uses beginIndex == 0, thus it extracts all the items from the collection

Array.prototype.slice.call(namespace, beginIndex, endIndex)

  • takes an object as the first argument. If we use a collection as an object it literally means that we call slice method directly from that object namespace.slice()
Constriction answered 13/4, 2018 at 18:16 Comment(7)
Thank you for this code snippet, which might provide some limited, immediate help. A proper explanation would greatly improve its long-term value by showing why this is a good solution to the problem and would make it more useful to future readers with other, similar questions. Please edit your answer to add some explanation, including the assumptions you’ve made.Carlinecarling
I'm wondering if this has IE support since Array.from does not. Time to find an IE machine. Now I'm really confused because I was able to use Array.from in IE10 and IE11 :\. This method does work in IE10+11 but I am not eased by Array.from working when all documentation says otherwise.Salop
Array.from does not work for me in IE11 Object doesn't support property or method 'from'Gennie
Thanks, this worked for me on an old implementation of JavaScriptLynlyncean
Array.from also returns a shallow copy. So I don't see how you conclude that it works faster than Array#slice.Longsighted
@Longsighted thanks for your comment, I've updated my answerConstriction
Wondering why not just Array.slice()? it's because slice is not a "class" function but a prototype function of the future Array object [] unlike e.g. Array.isArray() which is a "class"(something like a "static") function and can be called directly on Array. Therefore there actually is no Array.slice, only Array.prototype.slice :)Capote
C
9

[...a].filter vs Array.from(a).filter

Not a "real" difference in performance, Array.from could be a very very tiny bit faster because you're not creating a new Array on the "JS level", but it happens directly in the native code.

Performance - consider not using either

However for performance (and to also avoid "Array-ing") you should consider why are you filtering a NodeList and where/how did you get it. In many cases, you just want a particular element either by id or by class or other CSS selector.

document.querySelectorAll is like 10x - 200x faster and works for any CSS selector
document.getElementById is even faster (but of course requires an id)

You can even optimize querySelectorAll or bypass "not-yet-known" case if you provide a pre-stored parent to look in, let me give you an example:

let mainbar = document.getElementById('mainbar');
mainbar.querySelectorAll('.flex--item');

is almost 10x faster than

Array.from(a).filter(el => el.classList.contains("flex--item"))

Also be aware that document.querySelectorAll('#mainbar .flex--item'); is still about 5x faster than Array filtering, but about 2x slower than pre-storing the parent with an id.

Besides better performance, you will also always get NodeList (it may be empty, but it still will be NodeList) and that goes for both document.querySelectorAll() and Element.querySelectorAll()

Capote answered 18/9, 2021 at 22:43 Comment(0)
S
6

How about this:

// Be evil. Extend the prototype.
if (window.NodeList && !NodeList.prototype.filter) {
  NodeList.prototype.filter = Array.prototype.filter;
}

// Use it like you'd expect:
const noClasses = document
  .querySelectorAll('div')
  .filter(div => div.classList.length === 0)

It's the same approach as mentioned in the MDN docs for NodeList.forEach (under 'Polyfill'), it works for IE11, Edge, Chrome and FF.

Subcartilaginous answered 23/1, 2020 at 15:44 Comment(1)
slight warning, now nodeList.filter will then give you an array instead of a node list. shouldn't be a problem, but easy to forget ^^Ineducable
A
2

Filter or map nodelists in ES6 I came out of this simple function. See https://developer.mozilla.org/fr/docs/Web/API/NodeList/entries#exemple

function filterNodeList(NodeList, callback) {
            if (typeof callback !== "function") callback = (i) => i; // Any better idea?
            const Result = document.createElement("div");
            
    //# No need to filter empty NodeList
            if (NodeList.length === 0) return NodeList;
        
            for (let i = 0; i < NodeList.length; i++) {
              if (callback(NodeList.item(i))){
                 Result.appendChild(NodeList.item(i));
                 i--; //If you don't want to clone
              }
            }
            return Result.childNodes;
     }
Accommodate answered 28/7, 2021 at 18:12 Comment(2)
To kip the elements in the DOM, you can clone them and kip referencies Result.appendChild(NodeList.item(i).cloneNode). Next before you work on them, you just have to go back and say dom, give me this type of node with this attribut :: Important! Node === NodeList....Accommodate
You need to clone the node or decrement i in the appendChild call or the code is incorrect.Mondragon
S
0

Using ECMAS 2016:

let nodes = [...document.querySelector('__SELECTOR__').childNodes].filter(item => item.nodeType === 1);
Shermy answered 18/1, 2022 at 12:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.