Blog Entry - 10th August 2008 - Programming - JavaScript

Inheritance in JavaScript - 2 - Super and Base


Introduction

Following on from my previous blog entry, I have been exploring the different ways in which the super (Java) and base (C#) keywords can be emulated in JavaScript. No particular reason.

super is a Java language feature, which operates in two ways:-

  • You call it as super() in a sub-class constructor, to call the super-class constructor.
  • You can also use it to call a method on the immediate super-class by using super.methodName().

In C# base is the equivalent:-

  • In the case of constructors, it implemented slightly differently. It is added as a qualifier to the sub-class constructor function function SubClassConstructor() : base() {}, will call the super-constructor instead.
  • In the case of methods, it applies in the same way as super - base.methodName().

I originally thought that this would be easy to emulate in JavaScript, and made a bold and untested assertion about this in a blog comment.

In my attempts to achieve this, I have explored the different ways of achieving the same end, from David Flanagan, to Dean Edwards, to Douglas Crockford, to my own version.

Ultimately I did manage something close, and the closest was derived from Dean Edward's method.

Structure of My Classes

In the examples below I use the following basic module pattern:-

/// HELPER FUNCTIONS //////////////////////////////////////

function Inherit(
	constructor /*: Function*/,         // CHILD CLASS
	superConstructor /*: Function*/     // PARENT CLASS
){
	// CODE TO MAKE ONE CLASS INHERIT FROM ANOTHER - SEE LATER
}

function AddMethod(
	constructor /*: Function*/,  	// THE CLASS
	method /* Function*/,		// THE METHOD
	methodName /*: Function*/	// OPTIONALLY, THE METHOD'S NAME
) /*: void*/
{
	// CODE TO ADD A METHOD TO A CLASS - SEE LATER
}

/// CLASS DEFINITION //////////////////////////////////////
var MyClass = (function()
{
	function MyClass()  // CONSTRUCTOR FUNCTION
	{

	}

	// SOME OF THE SOLUTIONS BELOW DEPEND ON REPLACING
	// WITH A WRAPPER FUNCTION, HENCE "MyClass = " 

	/* MyClass = */ Inherit(MyClass, ParentClass);

	function method()  // A METHOD
	{

	}

	AddMethod(MyClass, method, "methodName");

	return MyClass;
})();

Dean's Method

Dean Edwards' method is to have a property at the top of the prototype chain called "base", which is dynamically modified when you call a method, so that it points to the overridden method of the current method you are calling.

Dean achieves this at method creation time, by wrapping your method in a wrapper function, which, when called at use time:-

  • sets the base property to the overridden method,
  • calls your method (which can then call the overridden method using this.base), and
  • then resets the base property to its previous value (as the previous value may still be in use by another function in the call stack).

Example 1

My example 1 below, gives you my version of Dean's implementation of this, and I also implement the same concept for the constructor function.

//// TEST FUNCTION /////////////////////////////////////////////////
//// CALLED AT THE END OF THIS FILE ////////////////////////////////

function test()
{	
	var dateOfManufacture = new Date(2001,00,01);
	var c = new A4(dateOfManufacture);
	
	alert(c.type()); // Car > Audi > A4
	alert(c.getDateOfManufacture());  // January 1st, 2001
 	
	alert(c.hasOwnProperty("dateOfManufacture"));  // ALL TRUE
	alert(c instanceof Car);
	alert(c instanceof Audi);
	alert(c instanceof A4);
	alert(c.constructor == A4);
}

//// HELPER FUNCTIONS /////////////////////////////////////////////////

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var proto /*: Object*/ = wrapper.prototype = new superConstructor();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = superConstructor.prototype;
	proto.constructor /*: Function*/ = wrapper;
	proto.superConstructor /*: Function*/ = superConstructor;

	function wrapper()
	{
		var previous = this.base;
		this.base = superConstructor;  // ALREADY WRAPPED IF EXTENDED
		var returnValue = constructor.apply(this, arguments);
		this.base = previous;
		return returnValue;
	}

	return wrapper;
}

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var f = function(){};
	f.prototype = superConstructor.prototype;

	var proto /*: Object*/ = wrapper.prototype = new f();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = f.prototype;
	proto.constructor /*: Function*/ = wrapper;
	proto.superConstructor /*: Function*/ = superConstructor;

	function wrapper()
	{
		var previous = this.base;
		this.base = superConstructor;  // ALREADY WRAPPED IF EXTENDED
		var returnValue = constructor.apply(this, arguments);
		this.base = previous;
		return returnValue;
	}

	return wrapper;
}

function AddMethod(
	constructor /*: Function*/,
	method /* Function*/,
	methodName /*: Function*/
) /*: void*/
{
	var methodName /*: String*/ = methodName || Function_GetIdentifier(method);
	var proto /*: Object*/ = constructor.prototype;
	var superProto /*: Object*/ = proto.superPrototype || null;
	var superMethod /*: Function*/ = superProto ? superProto[methodName] : null;

	if (superMethod)
	{
		function wrapper()
		{
			var previous /*: Function*/ = this.base;
			this.base = superMethod;
			var returnValue /*: Any*/ = method.apply(this, arguments);
			this.base = previous;
			return returnValue;
		}
	
		proto[methodName] = wrapper;
	}
	else
	{
		proto[methodName] = method;
	}
}

function Function_GetIdentifier(f /*: Function*/) /*: String*/
{
	return /^\s*function\s*(\w*)/.exec(f.toString())[1] || "anonymous";
}

//// BASE OBJECT /////////////////////////////////////////////////
//// MUST BE INHERITED BY ALL CLASSES ////////////////////////////

var Base = (function()
{
	function Base()
	{
		this.base = null;
	}

	// REPLACE Base WITH wrapper
	Base = Inherit(Base, Object);

	return Base;
})();

//// CAR ////////////////////////////////////////////////////////

var Car = (function()
{
	function Car(dateOfManufacture)
	{
		this.isCar = true;
		this.dateOfManufacture = dateOfManufacture || null;
	}

	Car = Inherit(Car, Base);

	function type() 
	{
		return "Car";
	}
	AddMethod(Car, type);

	function getDateOfManufacture() 
	{
		return this.dateOfManufacture;
	}
	AddMethod(Car, getDateOfManufacture);

	return Car;
})();

//// AUDI ////////////////////////////////////////////////////////
var Audi = (function()
{

	function Audi(dateOfManufacture)
	{
		this.isAudi = true;
		this.base(dateOfManufacture);
	}

	Audi = Inherit(Audi, Car);

	function type()
	{
		return this.base() + " > " + "Audi";
	}

	AddMethod(Audi, type);
	
	return Audi;
})();

//// A4 ////////////////////////////////////////////////////////
var A4 = (function()
{
	function A4(dateOfManufacture)
	{
		this.isA4 = true;
		this.base(dateOfManufacture);
	}

	A4 = Inherit(A4, Audi);

	function type() 
	{
		return this.base() + " > " + "A4";
	}

	AddMethod(A4, type);

	return A4;
})();

//// TRIGGER TEST ////////////////////////////////////////////////////////
test();

editplay

Example 2

In my next example below, I have almost lived up to my rash promise and implement the super method in the same manner as Java.

I.e.

  • "super()" should call the constructor for the inherited class
  • "super.method()" should call a method defined on the inherited class next up the chain

The best I can get to at the moment is:-

  • "this.base()" calls the constructor
  • "this.base.method(this)" calls the first function with the name "method" defined on any prototype further up the chain.

You have to supply the this object, otherwise the this object for this.base.method() would be base not this, because the program would only see base.method()``

I also have included an additional ability to call this.base.base if you want to skip a level!

This whole approach is probably quite memory expensive, as you have to create a whole bunch of wrapper functions for all your methods.

Finally, if you have a static method that has the same name as a method, it may override the static method on the constructor. Cloning the function is difficult, because we are using wrappers with closures. Actually, cloning is easy, because all we need to do is reproduce the wrapper function!

//// TEST FUNCTION /////////////////////////////////////////////////
//// CALLED AT THE END OF THIS FILE ////////////////////////////////

function test()
{	
	var dateOfManufacture = new Date(2001,00,01);
	var c = new A4(dateOfManufacture);
	c.setSpeed(100);
	
	alert(c.type()); // Car > Audi > A4
	alert(c.getDateOfManufacture());  // January 1st, 2001
	alert(c.getSpeed());	
	alert(c.hasOwnProperty("speed"));	 
}

//// HELPER FUNCTIONS /////////////////////////////////////////////////

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var f = function(){};
	f.prototype = superConstructor.prototype;

	var proto /*: Object*/ = wrapper.prototype = new f();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = f.prototype;
	proto.constructor /*: Function*/ = wrapper;
	proto.superConstructor /*: Function*/ = superConstructor;

	function superWrapperClone()
	{
		var previous = this.base;
		this.base = proto.superPrototype.base; 
		var returnValue = superConstructor.apply(this, arguments);
		this.base = previous;
		return returnValue;
	}

	proto.base /*: Function*/ = superWrapperClone;
	superWrapperClone.base = proto.superPrototype.base;

	function wrapper()
	{
		var previous = this.base;
		this.base = proto.base; 
		var returnValue = constructor.apply(this, arguments);
		this.base = previous;
		return returnValue;
	}

	return wrapper;
}

function AddMethod(
	constructor /*: Function*/,
	method /* Function*/,
	methodName /*: Function*/
) /*: void*/
{
	var methodName /*: String*/ = methodName || Function_GetIdentifier(method);
	var proto /*: Object*/ = constructor.prototype;
	var superProto /*: Object*/ = proto.superPrototype || null;
	var superMethod /*: Function*/ = superProto ? superProto[methodName] : null;

	proto.base[methodName] = function(thisArg) {
		var superArgs = Array.prototype.slice.call(arguments, 1);
		return superMethod.apply(thisArg, superArgs);
	};

	function wrapper()
	{
		var previous /*: Function*/ = this.base;
		this.base = proto.base;
		var returnValue /*: Any*/ = method.apply(this, arguments);
		this.base = previous;
		return returnValue;
	}
	
	proto[methodName] = wrapper;
}

function Function_GetIdentifier(f /*: Function*/) /*: String*/
{
	return /^\s*function\s*(\w*)/.exec(f.toString())[1] || "anonymous";
}

//// BASE OBJECT /////////////////////////////////////////////////
//// MUST BE INHERITED BY ALL CLASSES ////////////////////////////

var Base = (function()
{
	function Base()
	{
		this.base = null;
	}

	// REPLACE Base WITH wrapper
	Base = Inherit(Base, Object);

	return Base;
})();

//// CAR ////////////////////////////////////////////////////////

var Car = (function()
{
	function Car(dateOfManufacture)
	{
		this.isCar = true;
		this.dateOfManufacture = dateOfManufacture || null;
		this.speed = 0;
	}

	Car = Inherit(Car, Base);

	function type() 
	{
		return "Car";
	}
	AddMethod(Car, type);

	function getDateOfManufacture() 
	{
		return this.dateOfManufacture;
	}
	AddMethod(Car, getDateOfManufacture);

	function getSpeed() 
	{
		return this.speed;
	}
	AddMethod(Car, getSpeed);

	function setSpeed(speed) 
	{
		return this.speed = speed;
	}
	AddMethod(Car, setSpeed);

	return Car;
})();

//// AUDI ////////////////////////////////////////////////////////
var Audi = (function()
{

	function Audi(dateOfManufacture)
	{
		this.isAudi = true;
		this.base(dateOfManufacture);
	}

	Audi = Inherit(Audi, Car);

	function type()
	{
		return this.base.type(this) + " > " + "Audi";
	}

	AddMethod(Audi, type);
	
	return Audi;
})();

//// A4 ////////////////////////////////////////////////////////
var A4 = (function()
{
	function A4(dateOfManufacture)
	{
		this.isA4 = true;
		this.base(dateOfManufacture);
	}

	A4 = Inherit(A4, Audi);

	function type() 
	{
		return this.base.type(this) + " > " + "A4";
	}

	AddMethod(A4, type);

	function getSpeed() 
	{
		return "The " + this.base.base.type(this) + " is travelling at " + this.base.getSpeed(this) + "mph";
	}

	AddMethod(A4, getSpeed);

	return A4;
})();

//// TRIGGER TEST ////////////////////////////////////////////////////////
test();

editplay

David's Method

David Flanagan implements super and base for methods only.

At method creation time, David checks to see if a method by the same name exists on any inherited prototype, and if so, add a reference to it as a property to the method you are adding.

To call the super method, he created a chain function (I use base in the following example) which takes the arguments object of the calling method and then is able to extract the super method from arguments.callee.superMethod.

In David's version, you pass the arguments object; in my implementation I pass the calling method function object, because my version does not use anonymous functions so I am able to use the function name or arguments.callee.

My implementation also lets me call the the base method on any method of my class, because all of the method names share the same closure.

My implementation has also been extended to apply the same principle to the constructor function.

Separately, it is worth noting that one can call superMethods to any depth by using method.superMethod.superMethod.

Overall, whilst this approach does not have quite the same flexibility as Dean's, it has the advantage of being a little cleaner, without lots of wrapping of functions.

Example

This is a basic implementation of David's approach.


//// TEST FUNCTION /////////////////////////////////////////////////

function test()
{	
	var dateOfManufacture = new Date(2001,00,01);
	var c = new A4(dateOfManufacture);
	c.setSpeed(100);
	
	alert(c.type()); // Car > Audi > A4
	alert(c.getDateOfManufacture());  // January 1st, 2001
	alert(c.getSpeed());	
	alert(c.hasOwnProperty("speed"));	 
}

//// HELPER METHODS /////////////////////////////////////////////////

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var f = function(){};
	f.prototype = superConstructor.prototype;

	var proto /*: Object*/ = constructor.prototype = new f();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = f.prototype;
	proto.constructor /*: Function*/ = constructor;
	proto.superConstructor /*: Function*/ = superConstructor;

	constructor.superConstructor = superConstructor;
}

function AddMethod(
	constructor /*: Function*/,
	method /* Function*/,
	methodName /*: Function*/
) /*: void*/
{
	var methodName /*: String*/ = methodName || Function_GetIdentifier(method);
	var proto /*: Object*/ = constructor.prototype;
	var superProto /*: Object*/ = proto.superPrototype || null;
	var superMethod /*: Function*/ = superProto ? superProto[methodName] : null;

	if (superMethod)
	{
		method.superMethod = superMethod;
	}
	
	proto[methodName] = method;
}

function Function_GetIdentifier(f /*: Function*/) /*: String*/
{
	return /^\s*function\s*(\w*)/.exec(f.toString())[1] || "anonymous";
}

//// BASE OBJECT /////////////////////////////////////////////////
var Base = (function()
{
	function Base()
	{
		this.base = null;
	}

	Inherit(Base, Object);

	function base(method)
	{
		var superMethod = method.superMethod || method.superConstructor;
		var superArgs = Array.prototype.slice.call(arguments, 1);
		return superMethod.apply(this, superArgs);
	}

	AddMethod(Base, base);

	return Base;
})();


//// CAR ////////////////////////////////////////////////////////

var Car = (function()
{
	function Car(dateOfManufacture)
	{
		this.isCar = true;
		this.dateOfManufacture = dateOfManufacture || null;
		this.speed = 0;
	}

	Inherit(Car, Base);

	function type() 
	{
		return "Car";
	}
	AddMethod(Car, type);

	function getDateOfManufacture() 
	{
		return this.dateOfManufacture;
	}
	AddMethod(Car, getDateOfManufacture);

	function getSpeed() 
	{
		return this.speed;
	}
	AddMethod(Car, getSpeed);

	function setSpeed(speed) 
	{
		return this.speed = speed;
	}
	AddMethod(Car, setSpeed);

	return Car;
})();

//// AUDI ////////////////////////////////////////////////////////
var Audi = (function()
{

	function Audi(dateOfManufacture)
	{
		this.isAudi = true;
		this.base(Audi, dateOfManufacture);
	}

	Inherit(Audi, Car);

	function type()
	{
		return this.base(arguments.callee) + " > " + "Audi";
	}

	AddMethod(Audi, type);
	
	return Audi;
})();

//// A4 ////////////////////////////////////////////////////////
var A4 = (function()
{
	function A4(dateOfManufacture)
	{
		this.isA4 = true;
		this.base(A4, dateOfManufacture);
	}

	Inherit(A4, Audi);

	function type() 
	{
		return this.base(arguments.callee) + " > " + "A4";
	}

	AddMethod(A4, type);

	function getSpeed() 
	{
		return "The " + this.base(type.superMethod) + " is travelling at " + this.base(getSpeed) + "mph";
	}

	AddMethod(A4, getSpeed);

	return A4;
})();

//// TRIGGER TEST ////////////////////////////////////////////////////////
test();

editplay

Doug's Method

Douglas Crockford's method involves giving your class an uber method, which allows you to call super methods using this.uber("methodName").

Here is his code:-

Function.method('inherits', function (parent) {
    var d = {}, p = (this.prototype = new parent());
    this.method('uber', function uber(name) {
        if (!(name in d)) {
            d[name] = 0;
        }        
        var f, r, t = d[name], v = parent.prototype;
        if (t) {
            while (t) {
                v = v.constructor.prototype;
                t -= 1;
            }
            f = v[name];
        } else {
            f = p[name];
            if (f == this[name]) {
                f = v[name];
            }
        }
        d[name] += 1;
        r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
        d[name] -= 1;
        return r;
    });
    return this;
});

He does this by storing (as a closure) an object I will call depth which is used to store, for each methodName how many times a function of thatname has been called using this.uber in the current call stack.

To illustrate:-

  • Assume I had four classes in a chain of ineritance: GrandGrandParent > GrandParent > Parent > Child.
  • Each of those classes has two methods: calculate and draw
  • depth["calculate"] starts at 0 and depth["draw"] starts at 0, and 0 refers to Parent.prototype.calculate(), which is the starting super method.
  • In a method I call for the first time this.uber("calculate"). this.uber will call Parent.prototype.calculate(). At the same time depth["calculate"] is increased by 1 (now 1)
  • In Parent.prototype.calculate() I again call this.uber("calculate"). this.uber will call GrandParent.prototype.calculate(). It does this by getting depth["calculate"] and walking up the chain of prototypes from Parent.prototype base on the size of depth["calculate"]. At the same time depth["calculate"] is increased by 1 (now 2)
  • And so on, with each successive call to this.uber("calculate") referring to the next occurrence of calculate() on the prototype chain. The depth["calculate"] value returns to 0 when the call stack returns.

Now there is one important thing to note about this method. If my GrandParent.prototype.calculate() calls this.uber("draw") and this is the first time that this.uber("draw") has been called in the current call stack, which draw() method will it call?

Answer: Parent.prototype.draw() even though it is GrandParent.prototype.calculate() making the call to this.uber. This is because, as far as this.uber is concerned, depth["draw"] is still 0 because this.uber("draw") it has not yet been called

Some further points to note:-

  • When using this method, this.uber is created afresh on the prototype of each class that inherits from another class. this.ubers on classes further up the chain are ignored.
  • In order to assist comparison with the other functions in this entry, I use base rather than uber.
  • You cannot use this method to call at arbitrary points on the prototype chain it must always be the next up.
  • If you want to call the constructor of a super class you must use this.base("constructor")
  • If any prototype on your protoype chain misses out a method, then uber calls the next one up twice. E.g. if Parent did not implement calculate, but GrandParent did, then GrandParent.prototype.calculate gets called twice, because the count on t only moves one prototype at a time, so it returns true for Parent.protoype (because v[name] will keep looking up the chain) as well as GrandParent.prototype.

Example 1

Basic implementation of Doug's method.

Also illustrates:-

  • Through getSpeed - If my GrandParent.prototype.calculate() calls this.uber("draw") and this is the first time that this.uber("draw") has been called in the current call stack, which draw() method will it call? Answer: Parent.prototype.draw()
  • Through type() - The behaviour that if one class in the chain does not implement a particular method, then its parent class's method will get called twice. You should see Base > Car > Car > A4 for type().
//// TEST FUNCTION /////////////////////////////////////////////////

function test()
{
	var dateOfManufacture = new Date(2001,00,01);
	var c = new A4(dateOfManufacture);
	c.setSpeed(100);
	alert(c.type()); // Car > Audi > A4
	alert(c.getDateOfManufacture());  // January 1st, 2001
	alert(c.getSpeed());	
	alert(c.hasOwnProperty("speed"));	 
}

//// HELPER METHODS /////////////////////////////////////////////////

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var f = function(){};
	f.prototype = superConstructor.prototype;

	var proto /*: Object*/ = constructor.prototype = new f();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = f.prototype;
	proto.constructor /*: Function*/ = constructor;
	proto.superConstructor /*: Function*/ = superConstructor;

	constructor.superConstructor = superConstructor;

	var depth = {};
	
	function base(
		methodName /*: String*/
	) /*: String*/
	{
	        if (!(methodName in depth)) 
		{
			depth[methodName] = 0;
		}
		
		var currentDepth = depth[methodName];
		var superMethod;
		var superProto = proto.superPrototype;
		var returnValue;
		
		if (currentDepth)
		{
			while(currentDepth)
			{
				superProto = superProto.superPrototype;
				currentDepth--;
			}
			superMethod = superProto[methodName];
		}
		else
		{
			superMethod = proto[methodName];

			if (superMethod === this[methodName])
			{
				superMethod = superProto[methodName];
			}
		}
		
		depth[methodName] += 1;
		returnValue = superMethod.apply(this, Array.prototype.slice.call(arguments, 1));
		depth[methodName] -= 1;
		return returnValue;
	}

	AddMethod(constructor, base, "base");
}

function AddMethod(
	constructor /*: Function*/,
	method /* Function*/,
	methodName /*: Function*/
) /*: void*/
{
	var methodName /*: String*/ = methodName || Function_GetIdentifier(method);
	var proto /*: Object*/ = constructor.prototype;
	proto[methodName] = method;
}

function Function_GetIdentifier(f /*: Function*/) /*: String*/
{
	return /^\s*function\s*(\w*)/.exec(f.toString())[1] || "anonymous";
}

//// BASE OBJECT /////////////////////////////////////////////////
var Base = (function()
{
	function Base()
	{
		this.base = null;
	}

	Inherit(Base, Object);

	function type() 
	{
		return "Base";
	}

	AddMethod(Base, type);

	return Base;
})();


//// CAR ////////////////////////////////////////////////////////

var Car = (function()
{
	function Car(dateOfManufacture)
	{
		this.isCar = true;
		this.dateOfManufacture = dateOfManufacture || null;
		this.speed = 0;
	}

	Inherit(Car, Base);

	function type() 
	{
		return this.base("type") + ">" + "Car";
	}
	AddMethod(Car, type);

	function getDateOfManufacture() 
	{
		return this.dateOfManufacture;
	}
	AddMethod(Car, getDateOfManufacture);

	function getSpeed() 
	{
		return this.speed;
	}
	AddMethod(Car, getSpeed);

	function setSpeed(speed) 
	{
		return this.speed = speed;
	}
	AddMethod(Car, setSpeed);

	return Car;
})();

//// AUDI ////////////////////////////////////////////////////////
var Audi = (function()
{

	function Audi(dateOfManufacture)
	{
		this.isAudi = true;
		this.base("constructor", dateOfManufacture);
	}

	Inherit(Audi, Car);

	function type()
	{
		return this.base("type") + " > " + "Audi";
	}

	// BUG? IF I OMIT THIS, ``Car.prototype.type`` is called twice
	// AddMethod(Audi, type);

	function getSpeed() 
	{
		return "The " + this.base("type") + " is travelling at " + this.base("getSpeed") + "mph";
	}

	AddMethod(Audi, getSpeed);

	
	return Audi;
})();

//// A4 ////////////////////////////////////////////////////////
var A4 = (function()
{
	function A4(dateOfManufacture)
	{
		this.isA4 = true;
		this.base("constructor", dateOfManufacture);
	}

	Inherit(A4, Audi);

	function type() 
	{
		return this.base("type") + " > " + "A4";
	}

	AddMethod(A4, type);

	function getSpeed() 
	{
		return this.base("getSpeed");
	}

	AddMethod(A4, getSpeed);

	return A4;
})();

//// TRIGGER TEST ////////////////////////////////////////////////////////
test();

editplay

Example2

Corrects the point noted above, that if a method is missing in the prototype chain, uber calls the next one up twice.


//// TEST FUNCTION /////////////////////////////////////////////////

function test()
{
	var dateOfManufacture = new Date(2001,00,01);
	var c = new A4(dateOfManufacture);
	c.setSpeed(100);
	alert(c.type()); // Car > Audi > A4
	alert(c.getDateOfManufacture());  // January 1st, 2001
	alert(c.getSpeed());	
	alert(c.hasOwnProperty("speed"));	 
}

//// HELPER METHODS /////////////////////////////////////////////////

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var f = function(){};
	f.prototype = superConstructor.prototype;

	var proto /*: Object*/ = constructor.prototype = new f();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = f.prototype;
	proto.constructor /*: Function*/ = constructor;
	proto.superConstructor /*: Function*/ = superConstructor;

	constructor.superConstructor = superConstructor;

	var depth = {};
	
	function base(
		methodName /*: String*/
	) /*: String*/
	{
	        if (!(methodName in depth)) 
		{
			depth[methodName] = 0;
		}
		
		var currentDepth = depth[methodName];
		var superMethod;
		var superProto = getSuperProtoWithMethod(proto, methodName);
		var returnValue;
		
		while(currentDepth)
		{
			superProto = getSuperProtoWithMethod(superProto, methodName);	
			currentDepth--;
		}
		
		if (!superProto)
		{
			throw("Super method '" + methodName + "' could not be found");
		}
		else
		{
			superMethod = superProto[methodName];
		}
		
		depth[methodName] += 1;
		returnValue = superMethod.apply(this, Array.prototype.slice.call(arguments, 1));
		depth[methodName] -= 1;
		return returnValue;

		function getSuperProtoWithMethod(proto, methodName)
		{
			while(proto.superPrototype)
			{
				proto = proto.superPrototype
			
				if (proto.hasOwnProperty(methodName))
				{
					return proto;
				}
			}

			return null;
		}
	}

	AddMethod(constructor, base, "base");
}

function AddMethod(
	constructor /*: Function*/,
	method /* Function*/,
	methodName /*: Function*/
) /*: void*/
{
	var methodName /*: String*/ = methodName || Function_GetIdentifier(method);
	var proto /*: Object*/ = constructor.prototype;
	proto[methodName] = method;
}

function Function_GetIdentifier(f /*: Function*/) /*: String*/
{
	return /^\s*function\s*(\w*)/.exec(f.toString())[1] || "anonymous";
}

//// BASE OBJECT /////////////////////////////////////////////////
var Base = (function()
{
	function Base()
	{
		this.base = null;
	}

	Inherit(Base, Object);

	function type() 
	{
		return "Base";
	}

	AddMethod(Base, type);

	return Base;
})();


//// CAR ////////////////////////////////////////////////////////

var Car = (function()
{
	function Car(dateOfManufacture)
	{
		this.isCar = true;
		this.dateOfManufacture = dateOfManufacture || null;
		this.speed = 0;
	}

	Inherit(Car, Base);

	function type() 
	{
		return this.base("type") + " > " + "Car";
	}
	AddMethod(Car, type);

	function getDateOfManufacture() 
	{
		return this.dateOfManufacture;
	}
	AddMethod(Car, getDateOfManufacture);

	function getSpeed() 
	{
		return this.speed;
	}
	AddMethod(Car, getSpeed);

	function setSpeed(speed) 
	{
		return this.speed = speed;
	}
	AddMethod(Car, setSpeed);

	return Car;
})();

//// AUDI ////////////////////////////////////////////////////////
var Audi = (function()
{

	function Audi(dateOfManufacture)
	{
		this.isAudi = true;
		this.base("constructor", dateOfManufacture);
	}

	Inherit(Audi, Car);

	function type()
	{
		return this.base("type") + " > " + "Audi";
	}

	// BUG? IF I OMIT THIS, ``Car.prototype.type`` is called twice
	// AddMethod(Audi, type);

	function getSpeed() 
	{
		return "The " + this.base("type") + " is travelling at " + this.base("getSpeed") + "mph";
	}

	AddMethod(Audi, getSpeed);

	
	return Audi;
})();

//// A4 ////////////////////////////////////////////////////////
var A4 = (function()
{
	function A4(dateOfManufacture)
	{
		this.isA4 = true;
		this.base("constructor", dateOfManufacture);
	}

	Inherit(A4, Audi);

	function type() 
	{
		return this.base("type") + " > " + "A4";
	}

	AddMethod(A4, type);

	function getSpeed() 
	{
		return this.base("getSpeed");
	}

	AddMethod(A4, getSpeed);

	return A4;
})();

//// TRIGGER TEST ////////////////////////////////////////////////////////
test();

editplay

My Method

The method I take is a variant of both David's and Doug's.

With my approach, when I attach a method to a prototype, I give that method an ownerProto property which refers to the prototype to which it is attached, and a name property for easy access to the name of the method.

I then have a Base class from which all others inherit, which has a base method.

When you use base, you supply the function you wish to call the super function for, and base simply looks up the prototype chain from ownerProto to find the next function with the same name.

If you want to call the base constructor, you use base(constructorFunction).

David's has the advantage of doing all the look-ups once at method creation time.


//// TEST FUNCTION /////////////////////////////////////////////////

function test()
{
	var dateOfManufacture = new Date(2001,00,01);
	var c = new A4(dateOfManufacture);
	c.setSpeed(100);
	alert(c.type()); // Car > Audi > A4
	alert(c.getDateOfManufacture());  // January 1st, 2001
	alert(c.getSpeed());	
	alert(c.hasOwnProperty("speed"));	 
}

//// HELPER METHODS /////////////////////////////////////////////////

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var f = function(){};
	f.prototype = superConstructor.prototype;

	var proto /*: Object*/ = constructor.prototype = new f();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = f.prototype;
	proto.constructor /*: Function*/ = constructor;
	proto.superConstructor /*: Function*/ = superConstructor;

	constructor.superConstructor  = superConstructor;
}

function AddMethod(
	constructor /*: Function*/,
	method /* Function*/,
	methodName /*: Function*/
) /*: void*/
{
	var methodName /*: String*/ = methodName || Function_GetIdentifier(method);
	var proto /*: Object*/ = constructor.prototype;
	
	method.name = methodName;
	method.ownerProto = proto;
	proto[methodName] = method;
}

function Function_GetIdentifier(f /*: Function*/) /*: String*/
{
	return /^\s*function\s*(\w*)/.exec(f.toString())[1] || "anonymous";
}

//// BASE OBJECT /////////////////////////////////////////////////
var Base = (function()
{
	function Base()
	{
		this.base = null;
	}

	Inherit(Base, Object);

	function base (
		callingFunction /*: Function*/
	)/*: Any*/
	{
		if (!callingFunction)
		{
			var callingFunction = arguments.callee.caller;
		}


		if (callingFunction.superConstructor)
		{	
			return callingFunction.superConstructor.apply(this, Array.prototype.slice.call(arguments, 1));
		}

		var ownerProto /*: Object*/ = callingFunction.ownerProto;
		var name /*: String*/ = callingFunction.name;
		var superProto /*: Object*/ = ownerProto.superPrototype;

		while (superProto && !superProto.hasOwnProperty(name))
		{
			superProto = superProto.superProto;
		}

		var superMethod /*: Function*/ = superProto[name];
		return superMethod.apply(this, Array.prototype.slice.call(arguments, 1));
	}

	AddMethod(Base, base);

	function type() 
	{
		return "Base";
	}

	AddMethod(Base, type);

	return Base;
})();


//// CAR ////////////////////////////////////////////////////////

var Car = (function()
{
	function Car(dateOfManufacture)
	{
		this.isCar = true;
		this.dateOfManufacture = dateOfManufacture || null;
		this.speed = 0;
	}

	Inherit(Car, Base);

	function type() 
	{
		return this.base(type) + " > " + "Car";
	}
	AddMethod(Car, type);

	function getDateOfManufacture() 
	{
		return this.dateOfManufacture;
	}
	AddMethod(Car, getDateOfManufacture);

	function getSpeed() 
	{
		return this.speed;
	}
	AddMethod(Car, getSpeed);

	function setSpeed(speed) 
	{
		return this.speed = speed;
	}
	AddMethod(Car, setSpeed);

	return Car;
})();

//// AUDI ////////////////////////////////////////////////////////
var Audi = (function()
{

	function Audi(dateOfManufacture)
	{
		this.isAudi = true;
		this.base(Audi, dateOfManufacture);
	}

	Inherit(Audi, Car);

	function type()
	{
		return this.base(type) + " > " + "Audi";
	}

	AddMethod(Audi, type);

	function getSpeed() 
	{
		return "The " + this.base(type) + " is travelling at " + this.base(getSpeed) + "mph";
	}

	AddMethod(Audi, getSpeed);

	
	return Audi;
})();

//// A4 ////////////////////////////////////////////////////////
var A4 = (function()
{
	function A4(dateOfManufacture)
	{
		this.isA4 = true;
		this.base(A4, dateOfManufacture);
	}

	Inherit(A4, Audi);

	function type() 
	{
		return this.base(type) + " > " + "A4";
	}

	AddMethod(A4, type);

	function getSpeed() 
	{
		return this.base(getSpeed);
	}

	AddMethod(A4, getSpeed);

	return A4;
})();

//// TRIGGER TEST ////////////////////////////////////////////////////////
test();

editplay

Conclusion

Ultimately the choice is yours. Each has their own advantages and disadvantages. Dean's for ultimate flexibility. David's for performance. Doug's because it does not involve adding any properties to the method function, so it is perhaps the least invasive. Mine, no reason at all.

I think I would probably err towards David's approach.

Final Fun

If you wanted to be as explicit as possible about what you were calling, then how about this for a final option. Adding to David's approach, I also alow you to call or apply arbitrary methods of any inherited class, explicitly.

//// TEST FUNCTION /////////////////////////////////////////////////

function test()
{	
	var dateOfManufacture = new Date(2001,00,01);
	var c = new A4(dateOfManufacture);
	c.setSpeed(100);
	
	alert(c.type()); // Car > Audi > A4
	alert(c.getDateOfManufacture());  // January 1st, 2001
	alert(c.getSpeed());	
	alert(c.hasOwnProperty("speed"));	 
}

//// HELPER METHODS /////////////////////////////////////////////////

function Inherit(
	constructor /*: Function*/,
	superConstructor /*: Function*/
) /*: void*/
{
	var f = function(){};
	f.prototype = superConstructor.prototype;

	var proto /*: Object*/ = constructor.prototype = new f();
	proto.prototype /*: Object*/ = proto;
	proto.superPrototype /*: Object*/ = f.prototype;
	proto.constructor /*: Function*/ = constructor;
	proto.superConstructor /*: Function*/ = superConstructor;

	var constructorName /*: String*/ = Function_GetIdentifier(constructor);
	constructor.name = constructorName;
	constructor.superConstructor = superConstructor;

	var superConstr = superConstructor;

	while(superConstr)
	{
		if (superConstr.name)
		{
			constructor[name] = superConstr.name;
		}

		superConstr = superConstr.superConstructor;
	}
}

function AddMethod(
	constructor /*: Function*/,
	method /* Function*/,
	methodName /*: Function*/
) /*: void*/
{
	var methodName /*: String*/ = methodName || Function_GetIdentifier(method);
	var proto /*: Object*/ = constructor.prototype;
	var superProto /*: Object*/ = proto.superPrototype || null;
	var superMethod /*: Function*/ = superProto ? superProto[methodName] : null;

	if (superMethod)
	{
		method.superMethod = superMethod;
	}
	
	proto[methodName] = method;
	constructor[methodName] = method;
}

function Function_GetIdentifier(f /*: Function*/) /*: String*/
{
	return /^\s*function\s*(\w*)/.exec(f.toString())[1] || "anonymous";
}

//// BASE OBJECT /////////////////////////////////////////////////
var Base = (function()
{
	function Base()
	{
		this.base = null;
	}

	Inherit(Base, Object);

	function base(method)
	{
		var superMethod = method.superMethod || method.superConstructor;
		var superArgs = Array.prototype.slice.call(arguments, 1);
		return superMethod.apply(this, superArgs);
	}

	AddMethod(Base, base);

	function apply(method, args)
	{
		return method.apply(this, args);
	}

	AddMethod(Base, apply);

	function call(method)
	{
		var args = Array.prototype.slice.call(arguments, 1);
		return method.apply(this, args);
	}

	AddMethod(Base, call);

	return Base;
})();


//// CAR ////////////////////////////////////////////////////////

var Car = (function()
{
	function Car(dateOfManufacture)
	{
		this.isCar = true;
		this.dateOfManufacture = dateOfManufacture || null;
		this.speed = 0;
	}

	Inherit(Car, Base);

	function type() 
	{
		return "Car";
	}
	AddMethod(Car, type);

	function getDateOfManufacture() 
	{
		return this.dateOfManufacture;
	}
	AddMethod(Car, getDateOfManufacture);

	function getSpeed() 
	{
		return this.speed;
	}
	AddMethod(Car, getSpeed);

	function setSpeed(speed) 
	{
		return this.speed = speed;
	}
	AddMethod(Car, setSpeed);

	return Car;
})();

//// AUDI ////////////////////////////////////////////////////////
var Audi = (function()
{

	function Audi(dateOfManufacture)
	{
		this.isAudi = true;
		this.call(Car, dateOfManufacture);
	}

	Inherit(Audi, Car);

	function type()
	{
		return this.call(Car.type) + " > " + "Audi";
	}

	AddMethod(Audi, type);
	
	return Audi;
})();

//// A4 ////////////////////////////////////////////////////////
var A4 = (function()
{
	function A4(dateOfManufacture)
	{
		this.isA4 = true;
		this.call(Audi, dateOfManufacture);
	}

	Inherit(A4, Audi);

	function type() 
	{
		return this.call(Audi.type) + " > " + "A4";
	}

	AddMethod(A4, type);

	function getSpeed() 
	{
		return "The " + this.call(Car.type) + " is travelling at " + this.call(Car.getSpeed) + "mph";
	}

	AddMethod(A4, getSpeed);

	return A4;
})();

//// TRIGGER TEST ////////////////////////////////////////////////////////
test();

editplay


Comment(s)


Sorry, comments have been suspended. Too much offensive comment spam is causing the site to be blocked by firewalls (which ironically therefore defeats the point of posting spam in the first place!). I don't get that many comments anyway, so I am going to look at a better way of managing the comment spam before reinstating the comments.


Leave a comment ...


{{PREVIEW}} Comments stopped temporarily due to attack from comment spammers.