Make Object-Oriented Programming Easier With Only Six Lines of JavaScript
Introduction
JavaScript, as we're told, is not a class-based object-oriented language. There's
little explicit support for the object-oriented notion of classes and inheritance.
However, everything you can do with classes and inheritance in an explicitly
class-based language like Java or C# can be done in JavaScript with
constructor functions and delegation through the prototype chain. These
relationships have to be set up programmatically at runtime using idiomatic
JavaScript. Given JavaScript's highly dynamic nature this turns out to be
pretty simple.
The subclass function
Simple nonetheless, I find it worthwhile to abstract the idiomatic JavaScript
away inside a function. This has the added bonus of making the inheritance
relationship more explicit in the code and it addresses a couple of JavaScript
"warts" in the process. Most of the JavaScript code I write is simple enough
that it doesn't need inheritance. Whenever I find I do need it, however, I always take
the time to write a function like the subclass function below. It's pretty
simple, clocking in at a mere 6 lines of non-trivial JavaScript.
function subclass(constructor, superConstructor)
{
function surrogateConstructor()
{
}
surrogateConstructor.prototype = superConstructor.prototype;
var prototypeObject = new surrogateConstructor();
prototypeObject.constructor = constructor;
constructor.prototype = prototypeObject;
}
Using the subclass function
Suppose we have two JavaScript constructor functions: NamedItem and
NamedNumberedItem, where we want to be able to use NamedNumberedItem
objects anywhere we use NamedItem objects (but not vice-versa). Furthermore,
we want NamedNumberedItem to inherit NamedItem functionality
rather than reimplementing it. In idiomatic JavaScript we'd express this relationship
something like:
NamedNumberedItem.prototype = new NamedItem("")
Using the subclass function above, the same relationship can be expressed as:
subclass(NamedNumberedItem, NamedItem);
Below is an example implementation of the NamedItem and NamedNumberedItem "classes"
using the subclass function. The example then creates instances of the two types of objects
and queries them to demonstrate that the subclass function really does what you think it does.
// ---------------------------------------- class NamedItem
function NamedItem(name)
{
this.name = name;
}
NamedItem.prototype.getDescription = function ()
{
return this.name;
}
// ---------------------------------------- class NamedNumberedItem
subclass(NamedNumberedItem, NamedItem);
function NamedNumberedItem(name, number)
{
NamedItem.call(this, name);
this.number = number;
}
NamedNumberedItem.prototype.getDescription = function ()
{
return this.name + ":" + this.number;
}
var namedItem = new NamedItem("foo");
var namedNumberedItem = new NamedNumberedItem("foo", 1);
alert(namedItem.getDescription());
alert(namedItem instanceof Object);
alert(namedItem instanceof NamedItem);
alert(namedItem instanceof NamedNumberedItem);
alert(namedItem.constructor);
alert(namedNumberedItem.getDescription());
alert(namedNumberedItem instanceof Object);
alert(namedNumberedItem instanceof NamedItem);
alert(namedNumberedItem instanceof NamedNumberedItem);
alert(namedNumberedItem.constructor);
There should be no surprises. However, we should compare the behavior of the same
code, only using idiomatic JavaScript to set up the inheritance relationship. I'll
do that next.
// ---------------------------------------- class NamedItem
function NamedItem(name)
{
this.name = name;
}
NamedItem.prototype.getDescription = function ()
{
return this.name;
}
// ---------------------------------------- class NamedNumberedItem
NamedNumberedItem.prototype = new NamedItem("");
function NamedNumberedItem(name, number)
{
NamedItem.call(this, name);
this.number = number;
}
NamedNumberedItem.prototype.getDescription = function ()
{
return this.name + ":" + this.number;
}
var namedItem = new NamedItem("foo");
var namedNumberedItem = new NamedNumberedItem("foo", 1);
alert(namedItem.getDescription());
alert(namedItem instanceof Object);
alert(namedItem instanceof NamedItem);
alert(namedItem instanceof NamedNumberedItem);
alert(namedItem.constructor);
alert(namedNumberedItem.getDescription());
alert(namedNumberedItem instanceof Object);
alert(namedNumberedItem instanceof NamedItem);
alert(namedNumberedItem instanceof NamedNumberedItem);
alert(namedNumberedItem.constructor);
As the example code demonstrates, the two subclassing methods behave essentially the
same way, but they do differ on two key points: The handling of instance variables in the prototype and
the handling of the constructor property. In both of these cases, I contend
that the behavior of the subclass function makes more sense and is less
likely to lead to confusion.
The Handling of Instance Properties Compared
The idiomatic JavaScript code
creates a NamedItem object to serve as the prototype object for all objects
created by the NamedNumberedItem constructor. This means that
NamedNumberedItem objects inherit all the properties of the NamedItem
prototype. Some of these properties are class-wide properties shared by all members of
the NamedItem "class", but other properties (the name property) is
specic to that one object that is serving as the prototype.
Of course the problem is that while these properties may be instance properties to the prototype
object, they act like class properties to all of NamedNumberedItem objects that
inherit them. This can be a problem when the inherited methods that work with those properties
expect them to act like instance properties. Imagine what can happen if such an inherited
property is a mutable value like an array. There's a big semantic difference between modifying
an array local to one object as opposed to modifying one shared by many objects.
Normally we get around this problem by having the subclass constructor chain to the
superclass constructor before it does anything else. This way the superclass
constructor gets to create instance properties that really are instance properties.
These instance properties completely hide the ones in the prototype. So those properties
may be superfluous but they're also harmless.
The subclass function sidesteps this whole issue. Instead of calling the
superclass constructor function to create the prototype it uses a "surrogate"
constructor instead. To create a proper prototype chain, the surrogate constructor
"borrows" the prototype from the superclass constructor. However, it defines no
instance properties and equally importantly takes no arguments, regardless of how
many the superclass constructor itself takes. This solves two problems -- there
are no superfluous instance-properties-that-aren't-really-instance-properties in
the prototype, and the superclass function is off the hook for knowing
how many and what kind of arguments to pass to the superclass constructor.
One nod that JavaScript does makes towards class-based object-oriented programming is
the instanceof operator. This operator evaluates true if the first
argument, an object, has been constructed by the second argument, a constructor
function. It will also return true if the constructor was used to construct the
object's prototype, which is to say that there's something like a subclass relationship
between the two constructors.
You might expect that the use of the surrogate constructor would confound the
instanceof operator. However, the instanceof operator
relies on the prototype chain to determine "class" membership, rather than on an
object's constructor property. There's a good reason for this, as
we'll see next.
The constructor Property
One place the two subclassing approaches disagree is the constructor property.
In both examples, the NamedItem object's constructor properties each
evaluate to NamedItem just as you would expect. In the example using the
subclass function, the NamedNumberedItem object's constructor
evaluates to NamedNumberedItem, again, just as you would expect. In the idiomatic
JavaScript example, however, the NamedNumberedItem object's constructor
property evaluates to NamedItem -- the superclass. This is surprising, but it's
also consistent with the ECMAScript standard.
This is because the constructor property is not an instance property, but rather lives
in the prototype. More precisely it lives in a prototype. The problem here is that it's
missing from the NamedNumberedItem prototype, which instead inherits the constructor property
defined in the NamedItem prototype -- the value of which is the NamedItem
constructor. The problem, very simply, is that JavaScript automatically assigns constructor properties
to the default prototypes that it generates for functions. If you replace the default
prototype with one of your own -- as we did when we "subclassed" NamedItem to
get NamedNumberedItem -- then you get a prototype without a constructor instance property.
The prototype object instead inherits the constructor property from its own prototype
object.
We could have corrected this after setting up the prototype, like so:
NamedNumberedItem.prototype = new NamedItem("");
NamedNumberedItem.prototype.constructor = NamedNumberedItem;
This is rarely done in practice. Since the constructor property is rarely used, maybe it
doesn't hurt anything. However, when inspecting an arbitrary object in JavaScript the
constructor property is the only really convenient way you have of telling what kind of
object you have. This can make debugging unneccesarily complicated and confusing.