JavaScript has some surprising strengths and weaknesses. Although I often wish it were more strongly-typed, it can be incredibly convenient to change the behavior of a class by reassigning a method, rather than defining an explicit subclass. Good JavaScript makes abundant use of anonymous functions and closure, and that part at least works quite well.
One serious weakness of the language, however, is that it is a bit too open-ended. We often want to use inheritance, defining a hierarchical structure of code-sharing. JavaScript doesn't have classes, but it does have prototypal inheritance, where multiple objects can share attributes (typically methods, which are simply function objects) via the prototype attribute. This attribute is assigned to the object's constructor before it is constructed, giving the object a semi-secret reference to the prototype (e.g., __proto__) that is used to query prototypal attributes when they are undefined on the instance.
Unfortunately, no one really agrees on the right way to use prototypal inheritance. The most common approach is:
function Graph() { ... }
function Tree() { ... }
Tree.prototype = new Graph();This does
mostly the right thing, because any attributes on the
new Graph() instance will be available to any
Tree instance via the prototype system. However, it shares too much; not only does it share any attributes assigned to
Graph's prototype, it shares any attributes assigned in the
Graph constructor. Worse yet, it shares them between
all instances of
Tree.
How can this go wrong? Well, imagine if the
Graph constructor initializes an array:
function Graph() {
this.adjacency = [];
}Now all of your
Tree instances share the same
adjacency array, and modifications to one will affect the others. Whoops.
One easy way to fix this is to call the superclass constructor in the subclass. JavaScript doesn't chain constructors for you automatically. However, it's very easy for you to do via the
call method on all functions:
function Tree() {
Graph.call(this);
}This assigns a new array to the
adjacency attribute of all new trees, thus short-circuiting the lookup into the shared prototype.
And yet, as you may have noticed, there is still something irksome about this (the most common) approach: the
Tree prototype has to construct a "default instance" of
Graph to share. But what if there's no such thing as a "default instance"? Don't we just want to share
Graph's prototype?
Tree.prototype = Graph.prototype;
From
Tree's perspective, this does exactly the right thing. Trees will no longer share a single
adjacency field through their prototype if you forget to chain the
Graph constructor. However, any subsequent assignments to the
Tree prototype will also be applied to
Graph. Thus,
Tree.prototype.root = function() { ... };assigns a
root method not only to all
Tree instances, but to all
Graphs as well. Whoops!
An elegant, if cryptic, solution by Douglas Crockford is the
beget object. This is a magic object that shares the same prototype as the superclass, but is not actually an instance of the superclass. This way, you avoid needing to construct a "default" instance of the superclass. I prefer a slight variant of his implementation, adding an
extend method to all functions:
Function.prototype.extend = function() {
function f() {}
f.prototype = this.prototype;
return new f();
};Now, when I'm defining
Tree, I say:
Tree.prototype = Graph.extend();
Et voilà! The good news is that now your code works correctly. The bad news is that because this is not built-in to JavaScript, most frameworks define their own inheritance mechanisms, making interoperability difficult or impossible.