Maintainable JavaScript: Don't modify objects you don't own

The first talk I gave after arriving at Yahoo! was entitled Maintainable JavaScript (video). As with most topics I write or speak about, I didn’t think it would be terribly controversial. The basis of the talk is that hacking around on your own and writing code in an enterprise environment are two different things. Web developers are truly unique in that none of us learned what we know in school; we all began as hobbyists one way or another and taught ourselves most (if not all) of what we know.

Professionalization

The professionalization of web development has been a difficult journey because of our disparate beginnings. Even those who end up at large companies such as Yahoo! inevitably began on their own, hacking around. Perhaps you were even “the web guy” at a small company and could do pretty much whatever you wanted. When the large companies started tapping this previously undiscovered resource, it brought a lot of hackers into a corporate environment where they were met with constraints. No longer a lone soldier in a small battle, all of these self-taught, self-directed individuals had to figure out how to work together as a team.

At the time that I gave the talk (2007), web development was evolving into front-end engineering and people were having trouble with the transition. Smart folks like Nate Koechley talked about the professionalization of front-end engineering (video) and how our discipline was evolving. My talk was aimed at the same goal: helping front-end engineers adapt to JavaScript development in a team environment by making sure that their code was as maintainable as possible.

Why can’t I modify objects I don’t own?

I still gets email and comments about Maintainable JavaScript, and the most popular question is, “why can’t I modify objects I don’t own?” JavaScript is, of course, a dynamic language that allows you to add and remove objects and their members at any point in time. For many, this is precisely why they enjoy the language: there are very few constraints imposed by the language. And I was telling them not to do this. Why?

Dependability

The simple explanation is that an enterprise software product needs a consistent and dependable execution environment to be maintainable. In other languages, you consider already-existing objects as libraries for you to use in order to complete your task. In JavaScript, people saw already-existing objects as a playground in which you could do anything you wanted. My point was that you should treat the already-existing JavaScript objects as you would a library of utilities. Don’t override methods, don’t add new methods, don’t remove existing methods.

When you’re the only one working on a project, it’s easy to get away with these types of modifications because you know them and expect them. When working with a team on a large project, making changes like this cause mass confusion and a lot of lost time. I still remember a bug that occurred while working on My Yahoo! because someone had overridden YAHOO.util.Event.stopEvent() to do something else. It took days to track this problem down because we all assumed that this method was doing exactly what it always did. Once we discovered this, we also found other bugs because the same method was being used in other places with its original intended usage…but of course, it wasn’t behaving in that way. Unraveling this was an incredible mess and I’d be very happy if no engineers ever had to go through a similar exercise.

Incompatible implementations

But developer confusion isn’t the only problem. Another peril of modifying objects that you don’t own is the possibility of naming collisions and incompatible implementations. Take a lesson from the history of the Prototype JavaScript library. John Resig wrote about this a while ago, so I’ll just quickly summarize. Prior to version 1.6, Prototype implemented its own document.getElementsByClassName() method long before it was part of HTML5 and long before any browser thought about implementing it natively. In addition, Prototype also added the each() method to Array objects. Thus, users of the Prototype library began writing code such as:

document.getElementsByClassName("myclass").each(doSomething);

This wasn’t a problem until the native document.getElementsByClassName() method was implemented. While Prototype’s version returned an instance of Array, the native implementation returns a NodeList object. Since NodeList doesn’t have an each() method, either natively or added by Prototype, the above coding pattern caused a JavaScript error when executed in browsers that had a native implementation of document.getElementsByClassName(). The end result is that users of Prototype had to upgrade both the library code and their own code; what a maintenance nightmare.

What if everyone did it?

Looking at a few isolated examples doesn’t really represent the enormity of the maintenance problem when you modify objects that you shouldn’t. To understand this point of view, it’s helpful to take a step back and look at moral philosophy (aka ethics). Moral philosophy is all about determining if an action is moral. There are many schools of thought on the topic, but I point towards a favorite modern philosopher, Immanuel Kant.

While I don’t want to get too deeply into moral philosophy and open this up for philosophical debate, Kant was famous for trying to determine “universal law” as the basis for moral action. In short, you can determine if an act is moral by asking, what would happen if everyone did it? For example, what if everyone cheated on a test? In that case, the test becomes useless, so this must not be a moral action.

Applying this same line of reasoning to the topic at hand, what if everyone on your team started modifying objects that they didn’t own? What if I went in and made modifications to document and so did everyone else on my team? What if everyone on the team created their own global variables? I hope that it’s obvious just how detrimental these actions could be to a team development environment.

Simply put: if everyone on your team modified objects that they didn’t own, you’d quickly run into naming collisions, incompatible implementations, and maintenance nightmares.

As a side note, I find Kant’s question incredibly relevant to any system that must scale. “What if everyone did it?” can really save you some trouble when considered as part of a technical design.

Conclusion

Maintainable code is code that you don’t need to modify when the browser changes. You don’t know how browser developers will evolve existing browsers and the rate at which those evolutions will take place. The code you write needs to continue working in future browsers and with future versions of JavaScript libraries without modification, and you cannot ensure that when you’re modifying objects that you didn’t create in the first place. The only code you can be certain will remain the same is the code you write yourself.

I can’t state this strongly enough: your code is not maintainable when it requires modifications to objects you didn’t create. Stepping down that path only leads to maintenance nightmares going forward.

P.S. If you’re interested in learning more, check out my presentation on Scalable JavaScript Application Architecture (video).

Comments

  1. Jerry Su

    s/down/don't/

    I remember this being one of the first lessons I learned at Yahoo. I don't remember you bringing Immanuel Kant into it though. Nice touch.

  2. Adam Crabtree

    Great real world scenario for the reasons to avoid modifying unowned objects! I'm absolutely guilty of modifying jQuery.ajax for queueing to prevent race conditions, etc...

    While I agree with your reasoning behind unowned object modification, just to comment on Kant's moral philosophy, it is rather naive. It assumes the ideal and dictates that the ideal is what should be followed and/or pursued as opposed to the real. It ignores the system, human nature, and fails once it is reinserted.

    In a world of unlimited resources in which nothing is constrained by reality (i.e., time, cost, prowess, competitors) ideals reign supreme. Socialism is a simple example of the failed philosophy of idealism's inability to address a real-world system. Requiring the benevolence of man, ignoring free will, what we might collectively achieve through cooperation is undermined by the reality of limited resources. Some individuals will always have more than others. I won't go further into how this continues to break down, but suffice it to say, Kant's philosophy of "universal law" is equivalent to a perpetual motion machine, perfect in theory, impossible and useless when the pressures of an external system are applied. It is what separates theorist from engineers. If I pursue ideals that work in theory, but fail in reality, I fail.

    Still agree with your comments on the JavaScript though. =)

  3. Jimmy de Smitz

    What if everyone did it? What if everyone recycled, was courteous to their fellow human beings and respected nature? What if development teams communicated more clearly with each other? What if development teams kept coding guidelines and API documentation that developers actually read and kept up to date? What if developers knew what they were doing and didn't try and use mootools, jquery and prototype+scripty on the exact same page because they all essentially do the same thing... What if most dev teams actually tested their applications before releasing them? What if people thought about what they were doing before they did it? In a perfect world eh?

    It's also up to developers in charge of a codebase to maintain their own browser support. If a new browser comes out, chances are you're going to have to make changes to your codebase, be it css, js or both, any way. developers should be testing on beta versions/ nightly builds to make sure that their current codebase isn't breaking on a upcoming release of a browser.

    To put the blame solely on Prototype or any framework/ library for extending the document object or a native prototype is a little preposterous. It's this type of avant garde thinking that is helping shape the future of JavaScript. I mean thanks to Prototype, Function.prototype.bind is going to be natively implemented in future JavaScript releases. So are the Prototype team then going to be blamed for their current version of Prototype being incompatible with the new native Function.prototype.bind, even though it was their idea to begin with?

    No framework is released without some bugs and very rarely can we get a framework that can survive a browser update, particularly one by MSIE, without being tweaked a little.

  4. Micah Jaffe

    Great post, great examples and reasoning! Getting people to see a larger reason behind methodology can be like trying to argue over the shade of a color, but it's so important for people to grok to help a team move forward. I'll have to try Kant more often ;)

    I've also really enjoyed reading through your responses and your own qui(zzes|rks) of JavaScript. Keep 'em up.

  5. Nicholas C. Zakas

    @Adam - I don't want to get into a philosophical debate, however, universal law is a great way to test the scalability of a system. In my scenario, universal law is not a state of perfection, but a state of imperfection. You must assume this state of imperfection in order to build systems that can scale to an enterprise level.

    @Jimmy - Please don't put words in my mouth. I didn't blame Prototype for anything, I merely used it as an example of the problems that can occur when you modify objects that you don't own. Prototype isn't the only place where this is a problem, certainly. Prototype is suffering from its own success: it created useful methods that people wanted to be native. If they had done so without modifying native objects, then no one would have to upgrade their code. I also fundamentally disagree that a library can't survive a browser update. YUI has traditionally not had such problems because of its self-contained nature.

  6. David Bruant

    While I agree with most of what is said in the architecture video, I still have couple of questions and reactions.

    First of all, you create a parallel between a module and a graphical zone in the HTML page (this is how you define modules from the examples you give when you separate areas in the Twitter page, for instance). In my opinion, this is a dangerous parallel. A module as defined in your slide has no relation to a graphical view. A "unit of functionality" may not provide a graphical view.

    I have another concern about the use of libraries and especially their use for DOM manipulation (in the broad sense containing DOM elements, but also DOM events, etc.). If the core of the web application is the only component which is allowed to use the library, it must be it which do the DOM manipulations. It seems to be in contradiction with the idea that the modules are the components which are the closest to the user and then the DOM.

  7. Garrett Smith

    "didn’t think it would be terribly controversial."

    I would speculate that giving a lecture to individuals who aren't really qualified to make strong objectionable criticism would result in a tepid response. Or, "get a bigger tank". We've discussed this on comp.lang.javascript and be sure: It was discovered to be controversial and a productive discussion ensued.

    I have included some comments that mention the dependency and forwards compatibility issues on the controversial document that I authored and titled: "Code Guidelines for Rich Internet Applications".

    I mention this document because the document mentions yet another compelling reason for not modifying objects you don't own. It mentions this other reason as the first reason. That reason is that it is usually less clear to the person reading the code where the additional modifications were made.

    "A piece of code that modifies objects it does not own is less clear to the client of the API where those changes are coming from. "

    http://jibbering.com/faq/no...

    The code guidelines document tersely covers more. It covers loose inferences such as those based on navigator.userAgent (don't), code comments (explain why, not what), formatting (spaces, not tabs), validation and conformance, methods (be short and do one thing), loops (how to avoid them and how to optimize them when you can't), and words of caution regarding host objects.

    There is more work that I feel should be done on the "design" section. Comments welcome on comp.lang.javascript.

  8. David Calhoun

    I think I understand the basics behind why not to modify these objects, but it still seems to me there are reasonable cases where it might be acceptable. One case being browsers that haven't yet implemented a new specification.

    For instance, if I want to use Date.now() in IE, it seems reasonable that I could extend the prototype of Date to add this feature based on the standard (instead of some arbitrary spec):

    if(!Date.now) {
    Date.now = function() {return (new Date().getTime())};
    }

    Now Date.now() will work fine in IE, and it won't overwrite browsers that implement this natively. I believe Crockford also implied that he supported this during one of his recent talks, when he was talking about browsers that didn't yet implement trim() for strings (he suggested adding trim to the String's prototype if I remember right). Not that Crockford is right all the time, but it gives support to the idea that this isn't always a bad thing.

    The other option of course is to use a JavaScript library, but I always like to use pure JS where possible, and would help enable that.

Understanding JavaScript Promises E-book Cover

Demystify JavaScript promises with the e-book that explains not just concepts, but also real-world uses of promises.

Download the Free E-book!

The community edition of Understanding JavaScript Promises is a free download that arrives in minutes.