Last time I talked about my implementation of JavaScript inheritance. Since then I’ve made a number of improvements – its now simpler, faster and easier to use.
In my post I pointed out some alternative, including one from Kevin Lindsey. Kevin’s implementation is my favorite because its the simplest and least intrusive solution.
It turns out our two implementation are quite similar – the main difference is that I added in a a callSuper method. This might not seem like much, but let’s look at an example to see how is dramatically changes the code you write.
Let’s assume we have a Pet object that inherits from a Animal object (I don’t use “class” since JavaScript doesn’t have classes, just prototypes).
Here is how you would code in using Kevin’s implementation:
function Pet(owner, name) { this.owner = owner Pet.baseConstructor.call(this, owner, name) } KevLinDev.extend(Pet, Animal) Pet.prototype.toString = function() { return Pet.superClass.toString.call(this) + "\n" + "My owner is " + this.owner }
Notice three things. First, the name of the current class is hard-coded into methods (for example, look at Pet.baseConstructor
). Second, calling an overridden constructor (Pet.baseConstructor
for example) is different than calling an overridden method (Pet.superClass
). Last, invoking a super method requires using JavaScript’s call function instead of a normal method call.
Now let’s add in a callSuper method and see what the code looks like:
function Pet(owner, name) { this.owner = owner this.callSuper(arguments.callee, name) } Pet.inherits(Animal) Pet.prototype.toString = function() { return this.callSuper(arguments.callee) + "\n" + "My owner is " + this.owner }
We’ve gotten rid of all the limitations mentioned above – but have added two.
This first is the need to pass arguments.callee as the first parameter. This is used by callSuper
to determine the currently executing method. Without this information, callSuper
cannot lookup the overridden super method.
In previous versions of JavaScript you don’t need this parameter because you could determine what method invoked a method by arguments.callee.caller
. However, caller has been deprecated in JavaScript. Thus this works in Firefox and Internet Explorer but not Safari, Konqueror, and if I remember correctly, Opera.
The second limitation is hidden – using callSuper
introduces a performance hit since it has to look up the calling method (a consequence of caller being deprecated). However, in my new improved implementation this performance hit has been eliminated for some common cases and in other cases it has been reduced by 50%. Thus it only comes into play if you’re creating hundreds and hundreds of objects.
Updated Implementation
So let’s look at the new implementation:
/* Copyright (c) 2006 Charlie Savage License: BSD: http://www.opensource.org/licenses/bsd-license.php */ Function.prototype.inherits = function(superConstructor) { /* Borrowed from Kevin Lindsey - create a dummy constructor to create prototypes. This is useful because it means that we don't have to modify the real constructors to have if-statements to guard against initialization if we are creating a prototype (versus a regular instance).*/ function CreatePrototype() {} CreatePrototype.prototype = superConstructor.prototype /* Reset the current constructor. This is an ugly hack required by the JavaScript prototype-chain implementation. */ this.prototype = new CreatePrototype() this.prototype.constructor = this // Save a reference to the superClass constructor this.superConstructor = superConstructor this.prototype.callSuper = function() { /* In IE and Firefox we can get the caller argument via arguments.callee.caller. However, for some reason this has been deprecated in JavaScript and is not supported by Safari or Opera. So we have to pass it in as a parameter. Yuck.*/ // var caller = arguments.callee.caller var caller = arguments[0] var args = new Array() for (var i=1; i<arguments.length; i++) args.push(arguments[i]) /* Figure out where we are in the inheritance hierarchy and the name of the method that was invoked. */ var currentConstructor = this.constructor var methodName = null while (methodName == null && currentConstructor != null) { // Shortcut - Is a constructor being called? if (caller === currentConstructor) return currentConstructor.superConstructor.apply(this, args) methodName = figureMethodName(currentConstructor.prototype, caller) currentConstructor = currentConstructor.superConstructor } if (!methodName) throw new Error("Could not find method: " + methodName + ".") if (!currentConstructor) throw new Error("Prototype does not have an ancestor: " + currentConstructor + ".") // Finally - execute the method return currentConstructor.prototype[methodName].apply(this, args) } } function figureMethodName(prototype, method) { for (var key in prototype) { if (prototype.hasOwnProperty(key) && prototype[key] === method) return key } return null }
And here is an example of using it:
// -- Animal -- function Animal(name) { if (!name) throw new Error('Must specify an animal name') this.name = name } Animal.prototype.toString = function() { return 'My name is ' + this.name } // -- Pet -- function Pet(owner, name) { this.owner = owner this.callSuper(arguments.callee, name) } Pet.inherits(Animal) Pet.prototype.toString = function() { return this.callSuper(arguments.callee) + "\n" + "My owner is " + this.owner } // -- Cat -- function Cat(owner, name) { this.callSuper(arguments.callee, owner, name) } Cat.inherits(Pet) Cat.prototype.toString = function() { return this.callSuper(arguments.callee) + '\n' + 'I eat mice' } var cat = new Cat('charlie', 'oba') alert(cat.toString())
CreatePrototype
The first thing that might look suspicious is CreatePrototype
. This is a brilliant idea I borrowed from Kevin. It neatly solves a common problem that bites all new JavaScript developers – and sometimes experienced ones who are forgetfully like yours truly!
Take a look at the Animal constructor:
function Animal(name) { if (!name) throw new Error('Must specify an animal name') this.name = name }
In traditional JavaScript inheritance, to have Pet inherit from Animal, you’d write the following code:
Pet.prototype = new Animal() Pet.prototype.constructor = Pet
When we try to create a new Animal as a prototype, the Animal constructor will throw an exception because it was not passed a name. To avoid this, you have to sprinkle ugly if-statements throughout your constructors to protect code that should only be called when creating a new instance versus prototypes.
CreatePrototype
solves the whole mess because it entirely skips calling the Animal constructor. Instead, new CreatePrototype()
creates a blank new object. However, that object’s __proto__ property is set to the Animal’s prototype thereby inheriting all its methods which is exactly what we want.
Let’s look at a diagram to see what this looks like:
The gray boxes with capital letters are constructors (JavaScript functions)
while the blue objects with underlined letters are instance of objects.
By using CreatePrototype()
, we replace animal with createPrototype which is nothing more than a blank object. But notice that the __proto__ properties are still correctly setup.
callSuper
So how does callSuper work?
First, we need to determine the name of the method being currently executed and what prototype contains it. Let’s use Cat.toString as an example – its defined as an anonymous function so it doesn’t actually have a name. Thus the need for the ugly callee parameter, which is pointer to the currently executing method:
Cat.prototype.toString = function() { return this.callSuper(arguments.callee) + "\n" + "I eat mice" }
Starting at the Cat prototype we climb up the inheritance tree until we find the calling method. To do this, we call figureMethodName
for each prototype, specifying the current prototype and method (i.e., caller).
function figureMethodName(prototype, method) { for (var key in prototype) { if (prototype.hasOwnProperty(key) && prototype[key] === method) return key } return null }
The call to hasOwnProperty
is quite important – we only want to return methods defined on the current prototype. If we return a method defined by a super class, then we’ll end up with infinite recursion. That happens because we’ll end up calling the method on a child prototype instead of the super prototype. Thus, when callSuper
is invoked again we’ll end up back in the same place (remember that each time we invoke callSuper we start at the bottom of the inheritance chain because this always refers to the current JavaScript object).
Once we’ve found the method name, then we simply invoke it on the superConstructor and let JavaScript do the rest.
Here is the updated code – and as usual all feedback is welcome.