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.
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:
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.
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.
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.
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.
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.
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); // undefinedThis 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 worksBecause 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.
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.
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.
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.
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 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); // trueType-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 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.
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.
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.
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:
No new properties or methods can be added to the object, but existing ones can be modified or deleted.
Same as prevent extension, plus prevents existing properties and methods from being deleted.
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 modeIn 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.
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.