ambient reality

pimping code and tech crap

Monday, March 06, 2006

Javascript Inheritance

Igor, I've done it! I've come up with the most pimp way, IMHO, to do inheritance in Javascript. After doing some research, I found lots of interesting approaches, but I didn't particularily like any of them.

Douglas Crockford and Prototype extend the built classes with helper functions which is can be dangerous. Kevin Lindsey's uses the more common method of setting the object's prototype to a new instance of the extended object, but incorporates a superclass for accessing the base class. Harry Fuecks seems to agree that this technique is not the best. Harry seems to like Troels Knak-Nielsen's technique, but I wanted something even cleaner, easier and self contained.

So, after some lots of thinking and coding, here's what I came up with:

function Class(obj){var f=function(){for(i in this._bi)this._bi[i]=0;if(typeof(this.construct)=='function')this.construct.apply(this,arguments);};f.prototype=obj;var fp=f.prototype;fp._bi=[];fp._bo=[];fp.base=function(fn){if(fn){var args=[];for(var i=1;i<arguments.length;i++)args[i-1]=arguments[i];for(var b=this._bo;this._bi['_'+fn]<b.length;this._bi['_'+fn]++)if(typeof(b[this._bi['_'+fn]][fn])=='function')return b[this._bi['_'+fn]++][fn].apply(this,args);}};if(arguments.length>1){for(var i=1;i<arguments.length;i++){var e=arguments[i].prototype,b=fp._bo.length;fp._bo[b]={};for(p in e){if(p!="base"&&p!="_bi"){if(fp[p]){if(p!="_bo"){fp._bo[b][p]=e[p];fp._bi['_'+p]=0;}else fp._bo=fp._bo.concat(e[p]);}else fp[p]=e[p];}}}}return f;}

Here it is expanded:

function Class(obj){
    var f=function(){
        for(i in this._bi)
            this._bi[i]=0;
        if(typeof(this.construct)=='function')
            this.construct.apply(this,arguments);
    };

    f.prototype=obj;
    var fp=f.prototype;
    fp._bi=[];
    fp._bo=[];
    fp.base=function(fn){
        if(fn){
            var args=[];
            for(var i=1;i<arguments.length;i++)
                args[i-1]=arguments[i];
            for(var b=this._bo;this._bi['_'+fn]<b.length;this._bi['_'+fn]++)
                if(typeof(b[this._bi['_'+fn]][fn])=='function')
                    return b[this._bi['_'+fn]++][fn].apply(this,args);
        }
    };

    if(arguments.length>1){
        for(var i=1;i<arguments.length;i++){
            var e=arguments[i].prototype,b=fp._bo.length;
            fp._bo[b]={};
            for(p in e){
                if(p!="base"&&p!="_bi"){
                    if(fp[p]){
                        if(p!="_bo"){
                            fp._bo[b][p]=e[p];
                            fp._bi['_'+p]=0;
                        }else
                            fp._bo=fp._bo.concat(e[p]);
                    }else
                        fp[p]=e[p];
                }
            }
        }
    }

    return f;
}

Basically, it's a function that build's your neato class. The first argument is an object for this class, then you can optionally pass in as many other objects which will be extended.

When this function is called, it creates a new anonymous function and then sets the function's prototype to the object defined by the first argument. Then it needs to create two arrays for dealing with base objects and the method to invoke the base method. Finally the class is extended with each additional object passed in.

I used Kevin Lindsey's example as the base for my examples. We first start by defining our base Person class. You'll notice the first argument I'm passing an inline JSON object. I prefer this because it looks clean and you could also eval() an XMLHttpRequest response inline. Ooh, sexy!

var Person=new Class(
    {
        construct:function(first, last){
            if (arguments.length > 0)
                this.init(first, last);
        },
        init:function(first, last){
            this.first=first;
            this.last=last;
        },
        print:function(){
            return this.first + "," + this.last;
        }
    }
);

var p=new Person("Chris", "Barber");
alert(p.print());

So far this will alert "Chris,Barber". Cool, so next extend the Person class and create the Employee class.

var Employee=new Class(
    {
        construct:function(first, last, id){
            if (arguments.length > 0)
                this.init(first, last, id);
        },
        init:function(first, last, id){
            this.base("init", first, last);
            this.id = id;
        },
        print:function(){
            var name = this.base("print");
            return this.id + ":" + name;
        }
    },
    Person
);

var e=new Employee("Chris", "Barber", 13);
alert(e.print());

Now we get "13:Barber,Chris. Finally we create the Manager class.

var Manager=new Class(
    {
        construct:function(first, last, id, department){
            if (arguments.length > 0)
                this.init(first, last, id, department);
        },
        init:function(first, last, id, department){
            this.base("init", first, last, id);
            this.department = department;
        },
        print:function(){
            var employee = this.base("print");
            return employee + " manages " + this.department;
        }
    },
    Employee
);

var m=new Manager("Chris", "Barber", 13, "Engineering");
alert(m.print());

Lastly we are alerted "13:Chris,Barber manages Engineering".

You can easily extend more than one class by just passing in more objects.

var myObj=new Object();
myObj.mystuff="goes here";

var MyShibbyClass=new Class(
    myObj,
    Widget,
    Gizmo,
    Something
);


I ran this through a bunch of tests and everything worked pretty good, but there are a few rules:
  • To call the super method, you must use this.base() and pass in at a minimum the mehod name
  • You must not call the this.base() more than once per method (like you'd do that anyway)
  • The constructor must be called "construct" (I wanted to call it "constructor", but it was a reserved word or something)
  • You should not name your methods using reserved words (i.e. tostring, constructor, class, etc.)
I've tested this in Firefox 1.5, Mozilla 1.7.5, Opera 9 and IE6 all on Windows XP and they all work. I'm curious how Safari or Konqueror handles this method.

As for performance, I haven't benchmarked it, but I have to believe this is pretty damn efficient as far as bandwidth and memory is concerned.

I don't know if this method has been done before, so let me know if this something new or old. Feel free to use it for whatever. Don't be evil!

0 Comments:

Post a Comment

<< Home