Sunday, February 8, 2015

An Introduction to Duck Typing using Arrays and Collections

Previously I discussed the ES 5 Properties feature and its relationship to the Browser DOM. The post focused on how useful it is when you can wrap, replace and otherwise configure your entire DOM, the way you want it, and the Browser will simply obey your commands. While this is possible in FireFox and IE, it isn't yet possible in Chrome. So this post will instead focus on something that is more universally available, duck typing the array methods and using them with DOM collections.

Array methods

You can interrogate the Array methods by going through the constructor to get the prototype. In our case, lets look at Array.prototype by using Object.getOwnPropertyNames. This will tell us all of the supported Array methods and properties. Its actually quite a large number, and many of them are very helpful.
["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", "concat", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach", "some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight", "entries", "keys"]
Most of them are useful for initial iteration, and most of them, once invoked the first time, will return a new array, allowing you to chain operations together. Combining map's, filter's and reduce operations can solve a large set of problems. It would be great if you could use those on HTMLCollection and other DOM types that aren't themselves Array's.

HTMLCollection

So what is an HTMLCollection? In the words of Dr Frankenstein, "It's Alive!!!". Unlike most collections that only mutate if you add/remove items from them, the HTMLCollection is a live view of the DOM. For instance, if you call getElementsByTagName("p") to get a collection back, then modify the DOM, your returned collection will already be updated to include the modifications made. No need to query getElementsByTagName again. You can see that here in this fiddle. Note, I also added in StaticNodeList using querySelectorAll to show the differences between a live and static collection. In the case of querySelectorAll, the returned collection does not automatically resolve new DOM mutations and the query must be re-made to get the latest results.

The next bit about HTMLCollection is that it inherits from Object. So by default, it does not get Array methods. It also has its own definition of the length property backed by the Browser itself and in the case of IE and FireFox this is a true ES 5 property, not a field. So it "looks like" an Array but is itself, not an array. If it "looks like" an Array is that good enough?

Executing Array methods on HTMLCollection

For this step we can use the call, apply or bind methods to redirect any Array method to use our own object as the this pointer. Further, because those methods are duck typed, and simply work with any object that "looks like" an Array, they'll function just as if you had used an actual Array.

So that is pretty neat, and you'll notice some inefficiencies are avoided by this approach. First, the length property is only read at the beginning of the forEach loop. So if you modify the DOM and add more things, you are guaranteed that your loop body will exit anyway since it doesn't fetch length on each iteration. This is similar to lifting the length call out of your for loops, which is common practice among seasoned web developers. Since forEach is a built-in, there may also be JIT time optimizations that would apply that may not apply when using your own for loop (though the reverse is also true so measurement in your scenario can be critical).

Some other common pit-falls are not avoided. For instance, if you mutate the DOM in a way that changes the contents ahead of you in the iteration, then you will see those changes. Including, if someone inserts an element, then you'll process that element, but you will miss an element now beyond your length.

This later issue is easy to avoid. You can turn any HTMLCollection into an Array. The number of ways is staggering, but the easiest might be to invoke the Array constructor passing each element as an argument. But how do you unwrap the collection? You don't need to, it is "like an array" so you can use it with the apply method. (Note: This is in the absence of Array.from which is an ES 6 API)

Browser Built-Ins

So, most JavaScript built-ins can work on a wide range of objects. That is due to their duck typed nature and also due to their being part of the programmable fabric that underlies script engine instance itself. As we start to build out APIs in the Browser itself, we have to ask how useful those APIs will be. The stringifier on Location for instance returns the current URL. This is useless unless you ARE an instance of Location because that built-in queries some built-in state. So the Browser introduces an additional level of restriction based on the types of objects. It validates the "this" pointer and if we can't find an object on which our method can execute we throw an exception. In IE we call this the "Invalid Calling Object" exception. Its pretty much just a TypeError to help you know we were unable to process your call. Chrome will throw an "Illegal Invocation" exception. Everyone has their own way of noting this situation. You can test this by invoking the postMessage API on a non-window this pointer. We are telling you, your object is not "window like" enough for us to continue. In this case, the message ports and underlying internal event pump can't be retrieved from the object you provided ;-)

This leads to an interesting paradigm though. As long as the underlying object can resolve the instance of check that we throw at it, then the methods can technically be used. Can you use this to your advantage? Of course. All elements in the IE world derive from the same top level class. That includes SVG, XML, XHTML, etc... This means that an HTMLElement prototype method can technically be used on an SVG or XML element type because the DNA is shared between the objects. So let's move the popular classList API from HTMLElement down to Element and then use it on some SVG objects.

Conclusion

For Interop and Compat reasons the initial shape of the DOM is sort of fixed to the expectations of the general web. But that shouldn't stop you from molding it to the needs of your application. While removing things from the DOM can cause mashable components to stop working, adding things can often cause them to START working, especially if there is a lack of Interop in that area or if the APIs in question are new. Also, some of the mutations you might make, may already be in scope for the Browser vendors. If you look at classList for instance, most browsers already support this on the Element prototype, so you can imagine any remaining Browser's might follow suit there.

No comments:

Post a Comment