Rethinking DOM Extensions

I was recently asked my opinion about extending the DOM with JavaScript. Most browsers allow access to the prototype on DOM types, like Node and Element. Some of the earliest JavaScript libraries, most famously Prototype.js, took advantage of that fact to add functionality onto the core DOM types.

DOM extension has been a dirty word in the JavaScript community ever since, and even the Prototype.js team has sworn it off for their 2.0 release. (Juriy Zaytsev details the reasons why in his excellent post, “What’s Wrong with Extending the DOM”.)

Not long ago, I’d have agreed wholeheartedly that DOM extension does more harm than good, but a recent mobile project has got me singing a very different tune: DOM extension may well be the future.

DOM extension has traditionally taken this form (‚Ķalbeit often with some additional code to support browsers that don’t allow DOM extensions.):

Element.prototype.addClassName = function(className) {
if (this.className.indexOf(className) == -1) {
this.className += ' ' + className;
}
};
var div = document.getElementById('myDiv');
div.addClassName('foo');

The appeal is writing simple, clean code that’s easy to read and that makes every element behave the same way.

The Arguments Against DOM Extension

Argument: DOM types like Element and Node aren’t guaranteed to be exposed by the browser

The DOM Level 2 spec defines interfaces for Element, Node, and the rest of the gang, but it doesn’t say that these types actually have to be exposed to JavaScript. In fact, up until Internet Explorer 8, IE didn’t expose these types.

Rebuttal: All modern browsers expose these types

I don’t know about you, but I don’t do much coding for Safari 2, and I try to avoid IE6 whenever possible. When you’re developing code for older browsers, this is a very valid concern. Going forward or targeting specific platforms, it’s not.

Take a site optimized for iPhone or Android (or even IE Mobile 6). Those browsers all support DOM extensions. A site targeted at Firefox, Safari 3+, Chrome, Opera or IE8+? All support DOM extensions.

Support for legacy browsers is an important consideration when developing a library, but it’s not when you’re coding for a site that isn’t supposed to be optimized for them. I know many of us are still locked into supporting old versions of IE, but the future is a party that IE6 and IE7 aren’t invited to, and it’ll be here sooner than you think.

Argument: Host objects don’t have to play by the rules

We generally assume that every property in JavaScript is writable and accessible, and that all JavaScript objects will behave the same way. Well, they don’t have to. Special exceptions are made for host objects – the objects backed by native code – with DOM type objects chief among them. As Zaytsev points out, host objects can throw errors when you try to access non-existent properties (instead of returning undefined) or assign to read-only properties.

Rebuttal: In practice, DOM host objects (mostly) play by the rules

This argument isn’t really applicable unless you’re coding for a browser that doesn’t actually support DOM extension through prototypes, and trying manually assign functions to individual instances of DOM objects. That’s a very painful and treacherous path, and not one that I’d encourage anyone to go down.

There are edge cases, like HTMLObjectElement not inheriting Element in IE, but the elements that these kinds of exceptions apply to just aren’t very common, and are usually already handled as special cases by JS code.

So what are we back to? Don’t try to use DOM prototype extension as a solution in browsers that don’t support it. Done and done.

Argument: Extending the DOM increases the possibility of name collisions exponentially

There are two main considerations here: The code that third party scripts introduce, and future additions that browsers will make to the host objects.

If you use multiple third party components, and each of them define a method Element#forEach, one is going to overwrite the other and very likely cause an error, either by the two wanting the function to behave slightly differently or by the two accepting different arguments.

Rebuttal: Control your code and prefix your extensions with $

The desire of the JavaScript community to use multiple JavaScript libraries on a single page has always bothered me. I’m not sure if I need to explain why you don’t want the user to download and execute 300KB of redundant code.

I don’t concern myself with name collisions because:

1) You shouldn’t use multiple libraries at a time
2) You should know everything that a third party file is adding to the global namespace
3) You can always change the names in two third party files so that they don’t conflict

You have control over the code that gets added to the page, so exercise it.

The other side of the argument is that the browsers may introduce new features of their own that conflict with your extension methods. And that’s a very real concern that you don’t have much control over.

But there is a good workaround:

If we make a convention to prefix host object extensions with a dollar sign, we’re virtually guaranteed that there won’t be a collision with a native method. I could imagine a browser one day defining Element.prototype.addClassName, but Element.prototype.$addClassName? Very, very unlikely.

The dollar sign has an almost toxic association with JavaScript libraries, and browsers aren’t going to go near it. Prefixing extension methods with it would keep them safe from a collision.

A Query-Object-Free, DOM-Extended Future

While the arguments against DOM extension do have merit, they just don’t have the same kind of weight in real, day-to-day development as they do for framework/library development. When you’re developing a library, you need to strive for compatibility across a broad spectrum of browsers and in many different runtime environments created by many different developers of many different skill levels—and JavaScript’s everything-is-global attitude doesn’t help.

But that doesn’t have to apply to you, making a site for a specific purpose, and knowing what components are going into it. And that’s what most of us do.

By extending the native prototypes, we can start to correct three major problems:

1) Developers forgetting how to use host objects and native code
2) Developers constantly reconstructing “query” abstraction objects at the cost of performance
3) Developers adding libraries to pages to accomplish simple tasks

A handful of simple extensions can make a world of difference in how you approach code:

Element.prototype.$addClass = function(className) {
if (this.indexOf(className) == -1) {
this.className += " " + className;
}
};
NodeList.prototype.$each = function(fn /*, thisp */) {
var thisp = arguments[1];
for (var i = 0, j = this.length; i < j; i++) {
fn.call(thisp, this[i], i, this);
}
return this;
};
NodeList.prototype.$bind = function(eventType, callback) {
this.$each(function(elem) {
elem.addEventListener(eventType, callback, false);
});
};
document.querySelectorAll("ul > li").$bind("click", function(e) {
this.$addClass("clicked");
this.removeEventListener("click", arguments.callee);
});

A new age of web development is dawning, ushered in by touch devices and smartphones that are aggressively adopting standards and adding new native functionality to the browser.

We’re quickly entering a time when we don’t have the same kinds of compatibility concerns that caused abstraction layers to rise in popularity. DOM extension is going to be widely supported in the near future (it already is, if you can afford to skip IE6 and IE7). When used responsibly, DOM extension can help us write code with the same grace as we do with abstraction layers like jQuery or Prototype.js—and without the extra weight.

I used to think of DOM extension as poor practice, but my mind’s changed: DOM extension is the next step in a future where JavaScript abstraction layers are things of the past.

No comments.

Leave a Reply