Leveraging Array.prototype 

to ease the pain of dropping jQuery

Nick Bottomley
Node & JavaScript

--

The push to stop using jQuery as a library dependency is great, but jumping head-first into the DOM APIs can be a little rough at times. Nonetheless, familiarity with the vanilla DOM is a crucial skill in a front-end JavaScript dev’s skillset. Plus, modern browsers support APIs that make typical jQuery operations like class manipulation, property/attribute setting or retrieval, and tree traversal fairly easy.

What you don’t have are easy, built-in ways to operate on collections of elements or nodes. Calls to document.querySelectorAll and document.getElementsByTagName return array-like objects that are basically devoid of useful methods. Specifically, both should return instances of HTMLCollection, but that’s not how it actually works — querySelectorAll returns an instance of NodeList in WebKit browsers. It’d be great if both of these were subclasses of Array, but, alas, they aren’t. So what to do?

Well, Array.prototype has all the goodness you want. One way is to take the functional approach and co-opt an Array.prototype method like so:

Mapping id attributes from a NodeList

Edit: Daniel makes a good point — this stuff is tricky!

This is exactly the same as what’s above, and might make things clearer:

This works exactly the same way

Since Array.prototype.map.call === Function.prototype.call, the two code snippets above work exactly the same way. I usually use the first way to make it more clear which Array.prototype method I’m using, but it somewhat obscures what’s going on. Now that I consider it, the second approach (directly above) is probably better going forward. There’s a good answer on StackOverflow which explains how .call and .bind work together. I also just came across an article on Smashing Magazine which demonstrates some approaches similar to those given here.

Here’s my own explanation of what’s going on

Function.prototype.call.bind( Array.prototype.map )

Generally, call() is used as a method of another function. Maybe you’ve used this trick before to transform an arguments object into a plain array.

var args = Array.prototype.slice.call( arguments )

Here, call() is used as a method of slice(). Presumably, the this keyword is used somewhere in the inner workings of call() to refer to the object of which it’s being used a method — in this case, Array.prototype.slice.

But when we use call() “nakedly” as Function.prototype.call, rather than as a method of a function, it doesn’t know what “this” should be set to. What function is it supposed to be calling? This is where bind() comes in. Check out Function.prototype.bind on MDN. The first sentence is

The bind() method creates a new function that, when called, has its this keyword set to the provided value

So ultimately, what the expression evaluates to is a version of call() where this is set to Array.prototype.map, a feat which we accomplish through bind(). If this explanation is unclear please do let me know!

/edit

You can do the same pattern for any other Array.prototype method you’d like to use. Also, this code can be made a bit shorter by replacing “Array.prototype” with simply “[]”. I just prefer this way because it’s a bit more clear what’s happening.

Alternately, you can create wrappers for your common DOM selection methods as shown here:

Creating DOM selection functions that return regular arrays.

Using Array.prototype.slice.call(arguments) to transform the array-like arguments object into an actual array is an old trick. These functions accomplish the same thing for DOM node collections. Both of these functions will return real arrays with all the useful methods you want. As mentioned above, the first line here—L11 in the code—could and probably should be written like so:

var slice = Function.prototype.call.bind( Array.prototype.slice );

A couple caveats to keep in mind when working with regular arrays, as opposed to NodeList/HTMLCollection:

  • NodeList and HTMLCollection are sometimes “live” — as in “they are automatically updated when the underlying document is changed.” When you map them to an array, you lose this feature. If you are coming from jQuery, this will be natural. If you’ve been using vanilla DOM APIs for a while, it might be unexpected. However, from some very basic testing in Chrome it seems to me that HTMLCollection tends to be live while NodeList does not (despite what MDN might say). With small but important discrepancies like this, you’re probably better off working with regular arrays anyway.
  • There is another, probably lesser known, piece of functionality you lose when converting HTMLCollection to Array. With an HTMLCollection, you can access members by either their index, or their id attribute (with the name attribute as a fallback). As such, you can do things like collection.item to get the member with id=“item”. Once you’re working with an array, you’d need to use filter (or something similar) to accomplish the same thing.

Yet another approach, I guess, is to start mucking around with the NodeList and HTMLCollection prototypes. That might look something like this:

Probably don’t do this.

Even if you’re OK with extending host objects, this is still probably not the best approach. First, you’re sort of sneakily returning an Array where it’d be more natural to expect a NodeList. Second, you’d have to implement all the methods you want on both NodeList and HTMLCollection to make sure everything behaves as expected.

However, the following might actually be fairly useful

We’re still augmenting the HTMLCollection and NodeList prototypes which isn’t ideal. However, this approach requires far less setup, and is generally more comprehensible than the others. Both previous options I described involve defining a bunch of functions that abstract away some fairly tricky prototype/call/bind stuff.

Eventually we’ll be able to do away with these workarounds. The DOM4 specification includes an Elements class that extends Array. Hooray! Instances of Node and of Elements will have .query() and .queryAll() methods that will search their context subtree(s) for matching elements, and return an Elements object with array methods. Pretty sweet right? Update: I recently came across this polyfill for Elements and the element query methods.

Thanks for reading. Find me on Twitter for any questions or comments.

--

--