$ is evil: JavaScript library integration problems

Why Prototype and other libraries -- except YUI -- can cause as many problems as they solve

Integration of large JavaScript libraries is an increasing problem for major web sites and serious applications. The famous Prototype library was a great innovation, but one that can cause as many problems as it solves. Why do so many libraries repeat Prototype's mistakes? And how can you avoid them in your own reusable code?


Open up Firebug -- you do use Firebug, right? -- and load up USA Today's home page while watching the Net tab. Click the JS button and you'll see that this major, high-traffic web site loads a couple dozen JavaScript files (plus several Ajax requests), many from different third-party vendors. They all do important things, like analytics, ad serving, and social networking features. JavaScript executes inline, and unless vendors, web developers, and library programmers are careful, it's only a matter of time before some serious name collisions occur, leading to profoundly hard-to-debug errors.

In fact, it happened to a major publishing company I work with when they tried to mix MooTools with a third-party vendor whose scripts relied on Prototype.They both define constructors like Class. Whichever loads last "wins" by enforcing its idea of what Class is; load Prototype after MooTools, and all your code that relies on MooTools will fail, and vice versa. This, to put it mildly, is a problem, especially once you're three fifths of the way done with a major project that relies on a given library, and you find out during integration testing that you've just lost a lot of time and money.

The problem is not the programmers doing the implementation. It's the library writers.

Disclaimer

As an aside, I'm not writing this to be purposefully controversial, but I am following in the tradition of famous (or infamous) articles like extends is evil. After I got over my shock, I realized the wisdom in that simple statement, and I hope this article will be a wake-up call to anyone who might conceivably have to integrate code from various sources, including open-source scripts, in a professional web site or application.

If you've got complete control over your environment, such as in a pure intranet application, this knol is not for you. But even if you're working on a low-traffic web site that will do some Ajax, have some pull-down menus, maybe serve up some ads or track analytics, you may want to consider what I have to say.

Finally, let me say that I have made the major mistake I'm describing below when writing my own code library, and it caused problems for a customer. So, "been there, done that" -- and I'd like to help you not make the mistakes I have.

Let's (not) step all over the core of JavaScript

Prototype is the about worst offender when it comes to making unsafe assumptions about code it will interact with. I was wowed by Prototype when I first saw it. The whole concept of adding methods to core JavaScript objects like String and Array impressed me so much I started to do it myself. Since I spent half my time in the Java world, I added methods like Array.prototype.contains and String.prototype.startsWith -- and loved it. Admittedly, it created a nice, clean, object-oriented syntax.

But it was doomed to failure.

My first clue should have been that when adding Prototype to an application with lots of existing code, it destroyed every place I used  for (member in object) loops over an associative array. And I like associative arrays. The problem is that Prototype (the library) modifies the prototype (template) for Object -- the granddaddy of every variable in a JavaScript program, including functions. The methods it adds became members of every associative array I declared, thus wreaking havoc in my loops.

Library writers should take a sacred oath: "I will not stomp on other scripts. I will not assume that it's A-OK to make broad assumptions and modify every variable that my users can declare."

Bad assumptions


Take for example my Array.contains method. Syntactically super-nifty:

Array.prototype.contains = function(match) {
    if (this.length) {
        for (var idx = 0; idx <
this.length; idx++) {
            if (
this[idx] == match) {
                return true;
            }
        }
    }
    return false;
  //if we get here, there's no match
}

This lets me write easy-to-read object-oriented code like this:
if (namesArray.contains("Glen Ford")) { alert("doofus detected") }

But, what if -- in my web site that integrates several scripts from different vendors -- another party expects Array.contains to take an array as an argument (or a variable list of arguments) and return true if any of them are found. If my library loads after theirs, I've probably just broken their system, causing me and/or my client a lot of headaches and gnashing of teeth.

This is why "I will not assume that it's A-OK to make broad assumptions and modify every variable that my users can declare."

And this kind of collision can occur with any variable (integer, function, whatever) that you declare in the global scope -- like the ubiquitous $() function (which by the way is totally different between Prototype and jQuery). Convert object extensions to utility functions, and namespace all your functions (as explained below), and you can proudly say, "I will not stomp on other scripts."

Playing nice with others


The YUI library set a great standard for namespacing and generally not making unsafe assumptions. Absolutely everything defined in any of the wide variety of .js files that make up the library fits neatly and safely into a single global variable -- the YAHOO object. If by some bizarre coincidence you already define YAHOO, you can easily host the YUI files and do a global search-and-replace on them to fix the collision. So it's incredibly safe to mix YUI with any library or thrid-party script. This is the appeal of Not Making Unsafe Assumptions.

To fully fix the problem the Prototype and I both independently created, we also have to find a way to get the functionality of Array.contains without the collision problem. As it turns out, there's no school like the old school for this. Back in the day, we wrote simple functions to do this kind of job -- functions like:
function arrayContains(theArray, theMatch) {...}

Combine that with the idea of collecting all your functions into a single object, and you have something like this (taken from a library I wrote called "Nyx"):

var NYX = {}  //this creates an empty object we can add stuff to

NYX.util = {} //a sub-object for a utility package

//note the use of JSON here to apply the Singleton pattern
NYX.util.array = {
    contains: function(theArray, match) {
        if (theArray.length) {
            for (var idx = 0; idx < theArray.length; idx++) {
                if (theArray[idx] == match) {
                    return true;
                }
            }
        }
        return false;  //if we get here, there's no match
    }
}

Now the if statement I wrote earlier would look like this:
if (NYX.util.array.contains(namesArray, "Glen Ford")) {alert("doofus detected")}

Not so bad. Not terribly unreadable. It's not quite as object-oriented, but who cares -- it solves the of problems I outlined above.

Conclusion

At worst...

...following the YUI example is easy, sophisticated, and readable.

At best...

...namespacing all your variables and Not Making Unsafe Assumptions will save you a world of headaches down the line, because it prevents subtle, hard-to-debug errors when functions and object are redefined after they are created, in a way that some of your code loves but some of it hates.

Comments