My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Wednesday, July 25, 2007

JavaScript prototypal inheritance using known libraries

JavaScript is a "quite old" scripting language but every day there's some news about its normal behaviour or some common code ... well, today is my round :-)

There are a lot of frameworks or libraries We use every day but I think every JS developer should know atleast basic JavaScript prototypal inheritance.
It seems to be the ABC of this wonderful scripting language but if You'll read this post I suppose You'll be surprised!

Let me start with a simple example about JavaScript inheritance, a constructor A with just one prototyped method and a sort of subclass B that would be extend A one.
So, this is the A constructor:

function A(){};
A.prototype.constructorIs = function(F){return this.constructor === F};

// alert((new A).constructorIs(A)); // true

This is a basic constructor example and every instance will inherit constructorIs method.
This one will return instance.constructor and compare them with a generic constructor Function.
Well, at this point We need just another constructor that would extend A one ... and its name is, for a real fantasy example, B.

function B(){};
B.prototype = new A;

Well done, this is a simple JavaScript inheritance example.
Every B instance will be an instanceof B and an instanceof A but there's a strange JavaScript behaviour with this kind of inheritance:
every instanceof B will have a constructor property that will be exactely inherited one!

alert((new B).constructor === A); // true!!!

Since prototype inheritance works in this way, last example code shows a correct behaviour.
This is true just because if new Function inherits from Function.prototype and every Object, as prototype parameter "is", is assigned by reference and not by a copy.

However JavaScript has not an official or crossbrowser way to get last inherited instance constructor so this behaviour should be a problem when You write a bit complex code or use multiple inherited constructors (someone call them classes).

At this point if You choose to implement a generic constructor extend method or function, I suppose You should clean this behaviour adding a better one ... or if You don't think so, please don't call that function/method extend and choose another name such injecting arbitraty methods or properties!

What I mean is that native JavaScript behaviour, using A and B constructors, is this one:

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // false
a instanceof A, // true
b instanceof A, // true
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));

As I've said, this behaviour is logically correct but I suppose that every Object Oriented developer would have atleast one more true in that boolean list:
an instanceof B should have a B constructor!

This is why a lot of people wrote something about prototypal inheritance but it seems that not everyone tested correctly expected extend behaviour, that should be:

b.constructorIs(B), // TRUE

to be able to use every instance constructor to call, or apply, methods or, why not, to call or apply constructor.prototype.constructor one!

// base constructor or generic Object prototype
// to call its super (parent) while there's one
Object.prototype.SuperCall = function(method){
var parent = this.parent,
result;
if(this.parent.prototype.parent)
this.parent = this.parent.prototype.parent;
result = parent.prototype[method].apply(this, [].slice.call(arguments, 1));
this.parent = parent;
return result;
};

// base constructor
function A(){};
A.prototype.init = function(){
this.isA = true;
return this;
};

// constructor B extend A
B = $extend(A, {init:function(){
this.isB = true;
return this.SuperCall("init");
}});

// constructor C extend B
C = $extend(B, {init:function(){
this.isC = true;
return this.SuperCall("init");
}});

// A, B and C instances
var a = (new A).init(),
b = (new B).init(),
c = (new C).init();

// inheritance test
alert([
a.isA, // true
a.isB, // undefined
a.isC, // undefined
b.isA, // true
b.isB, // true
b.isC, // undefined
c.isA, // true
c.isB, // true
c.isC // true
].join("\n"));

Above example uses a parent property that should easyly be a shortcut for this.constructor.prototype.constructor.
If constructor is correctly implemented there's no reason to don't use multiple inheritance from last constructor to first extended one but this is possible only with correct extend method.

This is my humil one, self packed to stay into 205 bytes ... it's behaviour is exactly expected one and it has been used to create above example.

$extend=function(b){var p="prototype",a=[].slice.call(arguments,1),f=function(){},i=0,k;f[p]=b[p];f[p]=new f;while(i<a.length){for(k in a[i])f[p][k]=a[i][k];++i};f[p].constructor=f;f[p].parent=b;return f};


To compare extend code I've not used last example but this simplest one:

function A(){};
A.prototype.constructorIs = function(F){return this.constructor === F};

B = $extend(A);

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // true
a instanceof A, // true
b instanceof A, // true
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));

that's exactly the same I've used to do famous frameworks/lybraries tests.
Let's start with first one?



jQuery - V 1.1.3.1
One of the best library I've ever used, intuitive and powerful but it seems that jQuery didn't understand correctly what an extend function should do.
I'm talking about $.extend one, used with more than a single argument:

function A(){};
A.prototype.constructorIs = function(F){return this.constructor === F};

B = $.extend(A,{});

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // true
a instanceof A, // true
b instanceof A, // true
b instanceof B, // true
a instanceof B, // true
a instanceof Function, // false
b instanceof Function // false

].join("\n"));

quite cool John, but if I extend a constructor B starting from A, how can be an instanceof A at the same time an instanceof B???



prototype - V 1.5.1.1
This is a framework that I've never loved but I know this is a great one!
This is prototype behaviour (probably I didn't understand Object.extend usage?) :

function A(){};
A.prototype.constructorIs = function(F){return this.constructor === F};

function B(){};
B.prototype = Object.extend(B.prototype, A.prototype);

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // true
a instanceof A, // true
b instanceof A, // false
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));

Well ... I suppose that this is not a real extend public static Object method ... please change its name!



MooTools - V 1.11
Another wonderful framework, developed by a really good stuff ... but probably old prototype framework logic isn't yet totally removed? :-)

var A = new Class({
constructorIs:function(F){
return this.constructor === F
}
}),
B = A.extend();

var a = new A,
b = new B;

alert([

a.constructorIs(A), // false
b.constructorIs(B), // false
a instanceof A, // true
b instanceof A, // true
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));

Ok Valerio, I know constructor is always a sort of baseMooClass one ... but there's no way to correct this strange way to inherit functions?



dojo - V 0.9
Great dojo toolkit framework, in its new beta version, is able to produce my strange code using SuperCall method with expected constructor but an inherited instance can't return false in a "instanceof Super" check, don't You agree?

function A(){};
A.prototype.constructorIs = function(F){return this.constructor === F};

B = dojo.extend(function(){}, A.prototype);

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // true
a instanceof A, // true
b instanceof A, // false
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));

Please dojo developers confirm us that this is an expected behaviour because it seems too much strange as behaviour :P




Ok, now it's time to speak about team or developers who probably did these tests many time before me, starting from Kevin Lindsey:

// Kevin Lindsey - Article of 2006/11/12 at 17:32:30
function A(){};
A.prototype.constructorIs = function(F){return this.constructor === F};

function B(){};

KevLinDev.extend(B,A);

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // true
a instanceof A, // true
b instanceof A, // true
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));

Well done Kevin and your old article is cool too :-)



Next developer is, obviously, Dean Edwards and his even old Base proposal:

// Base - V 1.0.2 (now serving 2.X)

var A = Base.extend({
constructorIs:function(F){
return this.constructor === F
}
}),
B = A.extend();

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // true
a instanceof A, // true
b instanceof A, // true
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));


Last big library that "probably" recieved Douglas Crockford help is YUI!:

// YUI! -. V 2.2.2

function A(){};
A.prototype.constructorIs = function(F){return this.constructor === F};

function B(){};

YAHOO.extend(B, A);

var a = new A,
b = new B;

alert([

a.constructorIs(A), // true
b.constructorIs(B), // true
a instanceof A, // true
b instanceof A, // true
b instanceof B, // true
a instanceof B, // false
a instanceof Function, // false
b instanceof Function // false

].join("\n"));


Well done guys and that's all folks!

3 comments:

  1. Wow, nice bit of research there! I agree that it's important for JS developers to understand the native prototypal inheritance model before using any of those mainstream libraries. Must say out of them all Moo Tools or Base are my favourites. Keep the great JavaScript posts coming, we are reading :-)

    ReplyDelete
  2. Just a note, MooTools inheritance is based originally on Dean Edwards Base and not on prototype's Class system. I don't see an issue that the constructor of an extended Class is Class and not the parent Class, since instanceof works correctly.
    Anyway, nice research.

    ReplyDelete

Note: Only a member of this blog may post a comment.