Tuesday, February 23, 2010

Five ways to create objects - Part 2: Inheritance

Let me start by saying I think inheritance is somewhat overrated in JavaScript. A lot of the inheritance you are going to need is already created for you: Function, String, Number etc. all inherit Object via its prototype.

I actually wonder if more energy goes into intellectual exercises around JavaScript inheritance than into using inheritance for real time solutions. Yes, JavaScript has an excellent inheritance mechanism but lets face it all those examples with furry animals, hierarchies of wheeled vehicles and the like have little real world application in client side coding.

How often do your new objects really need to inherit from other new objects you have created? By its nature the client object modelling is essentially flat (like your monitor). If you find yourself in JavaScript creating complex java-style object models with layers of inheritance then you might want to ask yourself why. Ajax allowed us to defer to the server rather when we used to have to clone our business/server logic on the client. I'd argue such complex data structures are best left to the server, because they perform better, are more easily distributed across subsystems and are probably more suited to classical OOP.

With that said, JavaScript does offer a very nifty inheritance strategy - there are no classes - objects inherit form objects. Period. Its clean and its simple.

So here goes..

Last time I demonstrated five ways to create objects in JavaScript. Now, as promised here's how to apply inheritance in each case. Clearly some cases are more useful than others.

Lets say our notepad from last week's example might need to inherit some properties from a more generic "panel" component.

1. Simple Object Literal


var myApp = {};
myApp.panel = {};
myApp.panel.toggleDisplay = function() {
    this.displayed = (this.displayed==="none")? "" : "none";
}


myApp.panel.defaultWidth = 300;
myApp.notepad = {};
myApp.notepad.writeable = true;
myApp.notepad.font = 'helvetica';
myApp.notepad.setFont = function(theFont) {
    myApp.notepad.font = theFont;
}


//OK not inheritance at all. But best we can do, since notepad has no relation to panel.
myApp.panel.toggleDisplay.apply(myApp.notepad);
myApp.notepad.defaultWidth = myApp.panel.defaultWidth;

2. Nested Object Literal


var myApp = {};
myApp.panel = {
    toggleDisplay : function() {
        this.displayed = (this.displayed==="none") ? "" : "none";
    },
    defaultWidth : 300
};


myApp.notepad = {
    writeable: true,
    font: 'helvetica',
    setFont: function(theFont) {
        this.font = theFont;
    }
};


//Same brute-force inheritance as example (1)
myApp.panel.toggleDisplay.apply(myApp.notepad);
myApp.notepad.defaultWidth = myApp.panel.defaultWidth;

3. Constructor using Object Literal (courtesy of Douglas Crockford)


var myApp = {};


myApp.Panel = function(defaultWidth ) {
    var that = {};
    that.defaultWidth = defaultWidth ;
    that.toggleDisplay = function() {
        that.displayed = (that.displayed==="none") ? "" : "none";
    }
    return that;
}




myApp.Notepad = function(defaultFont, width) {
    var that = myApp.Panel(300);
    that.writeable = true;
    that.font = defaultFont;
    that.setFont = function(theFont) {
        that.font = theFont;
    }
    return that;
}


//true inheritance without using new or prototype (courtesy of Douglas Crockford)
myApp.notepad1 = myApp.Notepad('helvetica',300);
myApp.notepad1.defaultWidth;
   

4. Simple Constructor for new


var myApp = {};


myApp.Panel = function(defaultWidth) {
    this.defaultWidth=defaultWidth ;
    this.toggleDisplay = function() {
        this.displayed = (this.displayed==="none") ? "" : "none";
    }
}


myApp.Notepad = function(defaultFont) {
    this.writeable = true;
    this.font = defaultFont;
    this.setFont = function(theFont) {
        this.font = theFont;
    }
}


myApp.notepad1 = new myApp.Notepad('helvetica');
//Without prototype we can only kluge inheritance here. Example (5) will fix it.
myApp.notepad1.defaultWidth; //undefined

5. Prototype with Constructor for new


//utility function
function deepClone(obj) {
    var clone = {};
    for(var i in obj) {
        if(typeof(obj[i])==="object") {
            clone[i] = obj[i].deepClone();
        } else {
            clone[i] = obj[i];
        }
    }
    return clone;
}
 myApp = {};


myApp.Panel = function(defaultWidth) {
    this.defaultWidth = defaultWidth;
}
myApp.Panel.prototype.toggleDisplay = function() {
    this.displayed = (this.displayed==="none") ? '' : "none";
    alert('display = ' + (this.displayed ? 'on' : 'off'));
}


myApp.Notepad = function(defaultFont,defaultWidth) {
    myApp.Panel.call(this,defaultWidth); //inject self into Panel constructor
    this.font = defaultFont;
}


//inherit from Panel....
//better to simply grab Panel's prototype rather than create new instance of Panel
myApp.Notepad.prototype = myApp.Panel.prototype.deepClone();


myApp.Notepad.prototype.writeable = true;
myApp.Notepad.prototype.setFont = function(theFont) {
    this.font = theFont;
}


//true inheritance - this time using prototype
myApp.notepad1 = new myApp.Notepad('helvetica',300);
myApp.notepad1.defaultWidth; //300

No comments:

Post a Comment