Sergio and the sigil

JavaScript: Not your father's inheritance model - Part 2

Posted by Sergio on 2009-06-12
This post is part of a series called JavaScript Demystified.

This particular chapter is further divided in two parts. Read Part 1.

Build your own hierarchy

Let's pretend we are building some scripts that deal with musical instruments. We could define our own Guitar class type like this:

//The constructor
function Guitar(brand, model) {
    this.brand = brand;
    this.model = model;
    this.strings = ['E', 'A', 'D', 'G', 'B', 'e'];
}

//Instance methods
Guitar.prototype = {
    play: function (chord) {
        alert('Playing ' + chord); 
    },
    toString: function () {
        return '(Guitar: ' + 
            this.brand + ' ' +
            this.model + ')';
    }
};

var guitar1 = new Guitar('Gibson', 'Les Paul');

What may not be apparent by just looking at the code for the first time is that guitar1's Prototype will be Guitar.prototype, which means that guitar1 inherits from Guitar.prototype. Also guitar1.constructor === Guitar.

When the last line in the above example is executed, the JavaScript runtime will take care of initializing a new object that has Guitar.prototype as its Prototype and makes its constructor property point to the Guitar function.

But what if we want to create a different type of guitars and still reuse the existing Guitar type. We could do this:

function BassGuitar(brand, model) {
    //call the constructor of our base type
    Guitar.apply(this, [brand, model] );
    //change or add anything we wish
    this.strings = ['E', 'A', 'D', 'G'];
}

//Copy the Prototype of our base type
BassGuitar.prototype = Object.create(Guitar.prototype);

//Override whatever we want:
BassGuitar.prototype.toString = function () {
    return '(BassGuitar: ' + 
        this.brand + ' ' +
        this.model + ')';
};

var bass1 = new BassGuitar('Peavey', 'Cirrus');
bass1.toString(); //=> '(BassGuitar: Peavey Cirrus)'
alert(bass1.strings); //=> [ 'E', 'A', 'D', 'G' ]

But there's a problem with the above code. There isn't a method Object.create(). Yeah, that's one of the design omissions in JavaScript. Any prototype-based language needs an easy way to let use create objects from other objects.

Thankfully JavaScript is also dynamically typed, so we can add that function ourselves. Here I'm borrowing from Douglas Crockford.

//add this to the very beginning of your scripts
if (typeof Object.create !== 'function') {
    Object.create = function (o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}

I'll leave the interpretation of the above function as an exercise to the reader. Once you understand what it is doing you will have mastered prototypes and constructors.

Handy Prototype tricks

Knowing what we know now and once again leveraging JavaScript's dynamism we can fix one of the things that has always annoyed me in JavaScript: the lack of a trim() method on strings.

String.prototype.trim = function () {
    return this.replace( /^\s*(\S*(\s+\S+)*)\s*$/, "$1"); 
}; 
var text = ' some user-entered value   ';
alert( text.trim() ); // => 'some user-entered value'

How about an easy way to pad numbers with zeroes?

Number.prototype.padLeft = function (width) {
    var text = this.toString();
    for(; text.length < width; ){
        text = '0' + text;
    }
    return text;
};

var num = 1234;
alert(num.padLeft(6)); // => 001234

Why do I need to know all this stuff?

Well, you don't. But why even bother writing JavaScript code if you're not willing to learn how it works?

As we have been seeing during this series, JavaScript bears only some syntax similarities with C# or Java. Underneath the surface it has been designed very differently. The web is riddled with attempts to mimic class-based inheritance in JavaScript only because programmers don't want to learn and leverage prototype-based logic.

My humble advice is that you will feel happier and smarter if you chose to learn how to use the language as it was intended.