Chapter 11. Don’t Modify Objects You Don’t Own

One unique aspect of JavaScript is that nothing is sacred. By default, you can modify any object you can get your hands on. It doesn’t matter if the object is developer-defined or part of the default execution environment—it’s possible to change that object as long as you have access to it. This isn’t a problem in a one-developer project, in which exactly what is being modified is always known by the one person who’s in control of all code. On a multiple-developer project, however, the indiscriminate modification of objects is a big problem.

What Do You Own?

You own an object when your code creates the object. The code that creates the object may not have necessarily been written by you, but as long as it’s the code you’re responsible for maintaining, then you own that object. For instance, the YUI team owns the YUI object, and the Dojo team owns the dojo object. Even though the original person who wrote the code defining the object may not work on it anymore, the respective teams are still the owners of those objects.

When you use a JavaScript library in a project, you don’t automatically become the owner of its objects. In a multiple-developer project, everyone is assuming that the library objects work as they are documented. If you’re using YUI and make modifications to the YUI object, then you’re setting up a trap for your team. Someone is going to fall in, and it’s going to cause a problem.

Remember, if your code didn’t create the object, then it’s not yours to modify, which includes:

  • Native objects (Object, Array, and so on)

  • DOM objects (for example, document)

  • Browser Object Model (BOM) objects (such as window)

  • Library objects

All of these objects are part of your project’s execution environment. You can use these pieces as they are already provided to you or create new functionality; you should not modify what’s already there.

The Rules

Enterprise software needs a consistent and dependable execution environment to be maintainable. In other languages, you consider existing objects as libraries for you to use to complete your task. In JavaScript, you might see existing objects as a playground in which you can do anything you want. You should treat the 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 modification because you know them and expect them. When working with a team on a large project, making changes like this causes mass confusion and a lot of lost time.

Don’t Override Methods

One of the worst practices in JavaScript is overriding a method on an object you don’t own, which is precisely what caused us problems when I worked on the My Yahoo! team. Unfortunately, JavaScript makes it incredibly easy to override an existing method. Even the most venerable of methods, document.getElementById(), can be easily overridden:

// Bad
document.getElementById = function() {
    return null;        // talk about confusing
};

There is absolutely nothing preventing you from overwriting DOM methods as in this example. What’s worse, any script on the page is capable of overwriting any other script’s methods. So any script could override document.getElementById() to always return null, which in turn would cause JavaScript libraries and other code that relies upon this method to fail. You’ve also completely lost the original functionality and can’t get it back.

You may also see a pattern like this:

// Bad
document._originalGetElementById = document.getElementById;
document.getElementById = function(id) {
    if (id == "window") {
        return window;
    } else {
        return document._originalGetElementById(id);
    }
};

In this example, a pointer to the original document.getElementById() is stored in document._originalGetElementById() so that it can be used later. Then, document.getElementById() is overridden to contain a new method. That new method may call the original in some cases, but in one case, it won’t. This override-plus-fallback pattern is at least as bad as the original, and perhaps worse because sometimes document.getElementById() behaves as expected and sometimes it doesn’t.

I have firsthand experience dealing with the fallout after someone overrides an existing object method. It occurred while I was working on the My Yahoo! team, because someone had overridden the YUI 2 YAHOO.util.Event.stopEvent() method 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, so we never traced into that method once we hit it in a debugger. Once we discovered the overridden method, 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, one that cost a lot of time and money on a big project.

Don’t Add New Methods

It’s quite easy to add new methods to existing objects in JavaScript. You need only assign a function onto an existing object to make it a method, which allows you to modify all kinds of objects:

// Bad - adding method to DOM object
document.sayImAwesome = function() {
    alert("You're awesome.");
};

// Bad - adding method to native object
Array.prototype.reverseSort = function() {
    return this.sort().reverse();
};

// Bad - adding method to library object
YUI.doSomething = function() {
    // code
};

There is little stopping you from adding methods to any object you come across. The big problem with adding methods to objects you don’t own is that you may end up with a naming collision. Just because an object doesn’t have a method right now doesn’t mean it won’t in the future. What’s worse is that if the future native method behaves differently than your method, then you have a maintenance nightmare.

Take a lesson from the history of the Prototype JavaScript library. Prototype was famous for modifying all kinds of JavaScript objects. It added methods to DOM and native objects at will; in fact, most of the library was defined as extensions to existing objects rather than by creating their own. The Prototype developers saw the library as a way of filling in JavaScript’s gaps. Prior to version 1.6, Prototype implemented a method called document.getElementsByClassName(). You may recognize this method, because it was officially defined in HTML5 to standardize Prototype’s approach.

Prototype’s document.getElementsByClassName() method returned an array of elements containing the specified CSS classes. Prototype also had added a method on arrays, Array.prototype.each(), which iterated over the array and executed a function on each item. This led to developers writing code such as:

document.getElementByClassName("selected").each(doSomething);

This code didn’t have a problem until HTML5 standardized the method and browsers began implementing it natively. The Prototype team knew the native document.getElementsByClassName() was coming, so they did some defensive coding similar to the following:

if (!document.getElementsByClassName) {

    document.getElementsByClassName = function(classes) {
        // non-native implementation
    };

}

So Prototype was defining document.getElementsByClassName() only if it didn’t already exist. That would have been the end of the issue except for one important fact. The HTML5 document.getElementsByClassName() didn’t return an array, so the each() method didn’t exist. Native DOM methods use a specialized collection type called NodeList, and document.getElementsByClassname() returned a NodeList to match the other DOM methods.

Because NodeList doesn’t have an each() method, either natively or added by Prototype, using each() caused a JavaScript error when executed in browsers that had a native implementation of document.getElementsByClassName(). The end result was that users of Prototype had to upgrade both the library code and their own code—quite the maintenance nightmare.

Learn from Prototype’s mistake. You cannot accurately predict how JavaScript will change in the future. As the standards have evolved, they have often taken cues from JavaScript libraries such as Prototype to determine the next generation of functionality. In fact, a native Array.prototype.forEach() method is defined in ECMAScript 5 that works much like Prototype’s each() method. The problem is that you don’t know how the official functionality will differ from the original, and even subtle differences can cause big problems.

Note

Most JavaScript libraries have a plugin architecture that allows you to safely add new capabilities to the libraries. If you want to modify a library, creating a plug-in is the best and most maintainable way to do so.

Don’t Remove Methods

It’s just as easy to remove JavaScript methods as it is to add then. Of course, overriding a method is one form of removing an existing method. The simplest way to eliminate a method is to set its name equal to null:

// Bad - eliminating a DOM method
document.getElementById = null;

Setting a method to null ensures that it can’t be called, regardless of how it was defined. If the method is defined on the object instance (as opposed to the object prototype), then it can also be removed using the delete operator:

var person = {
    name: "Nicholas"
};

delete person.name;

console.log(person.name);       // undefined

This example removes the name property from the person object. The delete operator works only on instance properties and methods. If delete is used on a prototype property or method, it has no effect. For example:

// No effect
delete document.getElementById;

console.log(document.getElementById("myelement"));  // stil works

Because document.getElementById() is a prototype method, it cannot be removed using delete. However, as seen in an earlier example, it can still be set to null to prevent access.

It should go without saying that removing an already existing method is a bad practice. Not only are developers relying on that method to be there, but code may already exist using that method. Removing a method that is in use causes a runtime error. If your team shouldn’t be using a particular method, mark it as deprecated, either through documentation or through static code analysis. Removing a method should be the absolute last approach.

Note

Not removing methods is actually a good practice for objects that you own, as well. It’s very hard to remove methods from libraries or native objects, because there is third-party code relying on that functionality. In many cases, both libraries and browsers have had to keep buggy or incomplete methods for a long time, because removing them would cause errors on countless websites.

Better Approaches

Modifying objects you don’t own is a solution to some problems. It usually doesn’t happen organically; it happens because a developer has come across a problem that object modification solves. However, there is almost always more than one solution to any given problem. Most computer science knowledge has evolved out of solving problems in statically typed languages such as Java. There are many approaches, called design patterns, to extending existing objects without directly modifying those objects.

The most popular form of object augmentation outside of JavaScript is inheritance. If there’s a type of object that does most of what you want, then you can inherit from it and add additional functionality. There are two basic forms of inheritance in JavaScript: object-based and type-based.

Note

There are still some significant inheritance limitations in JavaScript. First, inheriting from DOM or BOM objects doesn’t work (yet). Second, inheriting from Array doesn’t quite work due to the intricacies of how numeric indices relate to the length property.

Object-Based Inheritance

In object-based inheritance, frequently called prototypal inheritance, one object inherits from another without invoking a constructor function. The ECMAScript 5 Object.create() method is the easiest way for one object to inherit from another. For instance:

var person = {
    name: "Nicholas",
    sayName: function() {
        alert(this.name);
    }
};

var myPerson = Object.create(person);

myPerson.sayName();     // pops up "Nicholas"

This example creates a new object myPerson that inherits from person. The inheritance occurs as myPerson’s prototype is set to person. After that, myPerson is able to access the same properties and methods on person until new properties or methods with the same name are defined. For instance, defining myPerson.sayName() automatically cuts off access to person.sayName():

myPerson.sayName = function() {
    alert("Anonymous");
};

myPerson.sayName();     // pops up "Anonymous"
person.sayName();       // pops up "Nicholas"

The Object.create() method allows you to specify a second argument, which is an object containing additional properties and methods to add to the new object. For example:

var myPerson = Object.create(person, {
    name: { 
        value: "Greg"
    }
});

myPerson.sayName();     // pops up "Greg"
person.sayName();       // pops up "Nicholas"

In this example, myPerson is created with its own value for name, so calling sayName() displays “Greg” instead of “Nicholas.”

Once a new object is created in this manner, you are completely free to modify the new object in whatever manner you see fit. After all, you are the owner of the new object, so you are free to add new methods, override existing methods, and even remove methods (or rather just prevent access to them) on your new object.

Type-Based Inheritance

Type-based inheritance works in a similar manner to object-based inheritance, in that it relies on the prototype to inherit from an existing object. However, type-based inheritance works with constructor functions instead of objects, which means you need access to the constructor function of the object you want to inherit from. You saw an example of type-based inheritance earlier in this book:

function MyError(message) {
    this.message = message;
}

MyError.prototype = new Error();

In this example, the MyError type inherits from Error, which is called the super type. It does so by assigning a new instance of Error to MyError.prototype. After that, every instance of MyError inherits its properties and methods from Error as well as now working with instanceof:

var error = new MyError("Something bad happened.");

console.log(error instanceof Error);        // true
console.log(error instanceof MyError);      // true

Type-based inheritance is best used with developer-defined constructor functions rather than those found natively in JavaScript. Also, type-based inheritance typically requires two steps: prototypal inheritance and then constructor inheritance. Constructor inheritance is when the super type constructor is called with a this-value of the newly created object. For example:

function Person(name) {
    this.name;
}

function Author(name) {
    Person.call(this, name);    // inherit constructor
}

Author.prototype = new Person();

In this code, the Author type inherits from Person. The property name is actually managed by the Person type, so Person.call(this, name) allows the Person constructor to continue defining that property. The Person constructor runs on this, which is the new Author object. So name ends up being defined on the new Author object.

As with object-based inheritance, type-based inheritance allows you flexibility in how you create new objects. Defining a type allows you to have multiple instances of the same object, all of which inherit from a common super type. Your new type should define exactly the properties and methods you want to use, and those can be completely different from the super type.

The Facade Pattern

The facade pattern is a popular design pattern that creates a new interface for an existing object. A facade is a completely new object that works with an existing object behind the scenes. Facades are also sometimes called wrappers, because they wrap an existing object with a different interface. If inheritance won’t work for your use case, then creating a facade is the next logical step.

Both jQuery and YUI use facades for their DOM interfaces. As mentioned previously, you can’t inherit from DOM objects, so the only option for safely adding new functionality is to create an facade. Here’s an example DOM object wrapper:

function DOMWrapper(element) {    
    this.element = element;
}

DOMWrapper.prototype.addClass = function(className) {
    this.element.className += " " + className;
};

DOMWrapper.prototype.remove = function() {
    this.element.parentNode.removeChild(this.element);
};

// Usage
var wrapper = new DOMWrapper(document.getElementById("my-div"));

// add a CSS class
wrapper.addClass("selected");

// remove the element
wrapper.remove();

The DOMWrapper type expects a DOM element to be passed into its constructor. That element is stored so that it can be referenced later, and methods are defined that work on that element. The addClass() method is an easy way to add CSS classes for elements not yet implementing the HTML5 classList property. The remove() method encapsulates removing an element from the DOM, eliminating the need for the developer to access the element’s parent node.

Facades are well suited to maintainable JavaScript, because you have complete control over the interface. You can allow or disallow access to any of the underlying object’s properties or methods, effectively filtering access to that object. You can also add new methods that are simpler to use than the existing ones (as is the case in this example). If the underlying object changes in any way, you’re able to make changes to the facade that allow your application to continue working.

Note

A facade that implements a specific interface to make one object look like it’s another is called an adapter. The only difference between facades and adapters is that the former creates a new interface and the latter implements an existing interface.

A Note on Polyfills

JavaScript polyfills (also known as shims) became popular when ECMAScript 5 and HTML5 features started being implemented in browsers. A polyfill implements functionality that is already well-defined and implemented natively in newer browsers. For example, ECMAScript 5 added the forEach() method for arrays. This method can be implemented using ECMAScript 3, so older browsers can use forEach() as if it were a newer browser. The key to polyfills is that they implement native functionality in a completely compatible way. Because the functionality exists in some browsers, it’s possible to test whether different cases are handled in a standards-compliant manner.

Polyfills often add new methods to objects they don’t own to achieve their end goal. I’m not a fan of polyfills, but I do understand why people use them. Polyfills are marginally safer than other types of object modification, because the native implementation already exists and can be worked with. Polyfills add new methods only when the native one isn’t present and the nonnative version behaves the same as the native one.

The advantage of polyfills is that you can easily remove them when you’re supporting only browsers with the native functionality. If you choose to use a polyfill, do your due diligence. Make sure the functionality matches the native version as closely as possible and double-check that the library has unit tests to verify the functionality. The disadvantage of polyfills is that they may not accurately implement the missing functionality, and then you end up with more problems rather than fewer.

For best maintainability, avoid polyfills and instead create a facade over existing native functionality. This approach gives you the most flexibility, which is especially important when native implementations have bugs. In that case, you never want to use the native API directly, because you can’t insulate yourself from the implementation bugs.

Preventing Modification

ECMAScript 5 introduced several methods to prevent modification of objects. This capability is important to understand, as it’s now possible to lock down objects to ensure that no one, accidentally or otherwise, changes functionality that they shouldn’t. This functionality is supported in Internet Explorer 9+, Firefox 4+, Safari 5.1+, Opera 12+, and Chrome. There are three levels of preventing modification:

Prevent extension

No new properties or methods can be added to the object, but existing ones can be modified or deleted.

Seal

Same as prevent extension, plus prevents existing properties and methods from being deleted.

Freeze

Same as seal, plus prevents existing properties methods from being modified (all fields are read-only).

Each of these lock-down types has two methods: a method that performs the action and a method that confirms the action was taken. For preventing extensions, Object.preventExtensions() and Object.isExtensible() are used:

var person = {
    name: "Nicholas"
};

// lock down the object
Object.preventExtensions(person);

console.log(Object.isExtensible(person));       // false

person.age = 25;    // fails silently unless in strict mode

In this example, person is locked down to the extension, so Object.isExtensible() is false. Attempting to assign a new property or method will fail silently in nonstrict mode. In strict mode, any attempt to add a new property or method to a nonextensible object causes an error.

To seal an object, use Object.seal(). You can determine whether an object is sealed using Object.isSealed():

// lock down the object
Object.seal(person);

console.log(Object.isExtensible(person));   // false
console.log(Object.isSealed(person));       // true

delete person.name; // fails silently unless in strict mode
person.age = 25;    // fails silently unless in strict mode

When an object is sealed, its existing properties and methods cannot be removed, so attempting to remove name will fail silently in nonstrict mode. In strict mode, attempting to delete a property or method results in an error. Sealed objects are also nonextensible, so Object.isExtensible() returns false.

To freeze an object, use Object.freeze(). You can determine whether an object is frozen using Object.isFrozen():

// lock down the object
Object.freeze(person);

console.log(Object.isExtensible(person));   // false
console.log(Object.isSealed(person));       // true
console.log(Object.isFrozen(person));       // true

person.name = "Greg";   // fails silently unless in strict mode
person.age = 25;        // fails silently unless in strict mode
delete person.name;     // fails silently unless in strict mode

Frozen objects are considered both nonextensible and sealed, so Object.isExtensible() returns false and Object.isSealed() returns true for all frozen objects. The big difference between frozen objects and sealed objects is that you cannot modify any existing properties or methods. Any attempt to do so fails silently in nonstrict mode and throws an error in strict mode.

Preventing modification using these ECMAScript 5 methods is an excellent way to ensure that your objects aren’t modified without your knowledge. If you’re a library author, you may want to lock down certain parts of the core library to make sure they’re not accidentally changed or to enforce where extensions are allowed to live. If you’re an application developer, lock down any parts of the application that shouldn’t change. In both cases, using one of the lock-down methods should happen only after you’ve completely defined all object functionality. Once an object is locked down, it cannot be restored.

If you decide to prevent modification of your objects, I strongly recommend using strict mode. In nonstrict mode, attempts to modify unmodifiable objects always fail silently, which could be very frustrating during debugging. By using strict mode, these same attempts will throw an error and make it more obvious why the modification isn’t working.

Note

It’s likely that in the future, both native JavaScript and DOM objects will have some built-in protection against modification using this ECMAScript 5 functionality.