this space intentionally left blank

September 25, 2012

Filed under: tech»web

DOM If You Don't

I've noticed, as browsers have gotten better (and the pace of improvement has picked up) that there's an increasingly vocal group of front-end developers crusading against libraries like jQuery, in favor of raw JavaScript coding. Granted, most of these are the fanatical comp.lang.javascript types who have been wearing tinfoil anti-jQuery hats for years. But the argument is intriguing: do we really need to include 30KB of script on every page as browsers implement dev-friendly features like querySelectorAll? Could we get away with writing "pure" JavaScript, especially on mobile where every kilobyte counts?

It's a good question. But I suspect that the answer, for most developers, will continue to be "no, use jQuery or Dojo." There are a lot of good reasons for this--including the fact that they deliver quite a bit more than just DOM navigation these days--but the primary reason, as usual, is simple: no matter how they claim to have changed, browser developers still hate you.

Let's take a simple example. I'd like to find all the file inputs in a document and add a listener to them for HTML5 uploads. In jQuery, of course, this is a beautifully short one-liner, thanks to the way it operates over collections: $('input[type=file]').on('change', onChange); Doing this in "naked" JavaScript is markedly more verbose, to the point where I'm forced to break it into several lines for readability (and it's still kind of a slog). var inputs = document.querySelectorAll('input[type=file]'); inputs.forEach(function(element) { element.addEventListener('change', onChange); }); Except, of course, it doesn't actually work: like all of the document methods, querySelectorAll doesn't return an array with JavaScript methods like slice, map, or forEach. Instead, it returns a NodeList object, which is array-like (meaning it's numerically-indexed and has a length property). Want to do anything other than a length check or an iterative loop over that list? Better break out your prototypes to convert it to a real JavaScript object: var inputs = document.querySelectorAll('input[type=file]'); inputs = Array.prototype.slice.call(inputs); inputs.forEach(function(element) { element.addEventListener('change', onChange); }); Oh, yeah. That's elegant. Imagine writing all this boilerplate every time you want to do anything with multiple elements from the page (then imagine trying to train inexperienced teammates on what Array.prototype.slice is doing). No doubt you'd end up writing yourself a helper function to abstract all this away, followed by similar functions for bulk-editing CSS styles or performing animations. Congratulations, you've just reinvented jQuery!

All this could be fixable if browsers returned native JavaScript objects in response to JavaScript calls. That would be the logical, sensical thing to do. They don't, because those calls were originally specified as "live" queries (a feature that no developer has ever actually wanted, and exists to implement the now-obsolete DOM-0 document collections), and so the list they return is a thin wrapper over a native host object. Even though querySelector and querySelectorAll are not live, and even we knew by the time of their implementation that this was an issue, they're still wrappers around host objects with the same impedance mismatch.

Malice or incompetence? Who knows? It looks to me like the people developing browser standards are too close to their rendering engines, and not close enough to real-world JavaScript development. I think it's useful for illustration purposes to compare the output of running a DOM query in Firebug vs. the built-in Firefox Web Console. The former gives you a readable, selector-based line of elements. The latter gives you an incomprehensible line of gibberish or a flat "[Object HTMLDivElement]", which when clicked will produce a clunky tree menu of its contents. Calling this useless is an insult to dead-end interfaces everywhere--and yet that's what Mozilla seems to think is sufficient to compete with Firebug and the Chrome dev tools.

At any given time, there are at least three standards committees fighting over how to ruin JavaScript: there's ECMA TC-39 (syntax), W3C (DOM standards), and WHATWG (HTML5). There are people working on making JavaScript more like Python or CoffeeScript, and people working on more "semantic" tags than you can shake a stick at. But the real problems with JavaScript--the reasons that everyone includes that 30KB of jQuery.js--do not have anything to do with braces or function keywords or <aside> tags. The problem is that the DOM is a horrible API, from elements to events, filled with boilerplate and spring-loaded bear traps. The only people willing to take a run at a DOM 2.0 require you to use a whole new language (which might almost be worth it).

So in the meantime, for the vast majority of front-end developers (myself included), jQuery and other libraries have become that replacement DOM API. Even if they didn't provide a host of other useful utility functions (jQuery's Deferred and Callback objects being my new favorites), it's still just too frustrating to write against the raw browser interface. And library authors recognize this: you can see it in jQuery's planned removal of IE 6, 7, and 8 support in version 2.0. With the worst of the cross-compatibility issues clearing up, libraries can concentrate on writing APIs to make browsers more pleasant to develop in. After all, somebody has to do it.

Past - Present