Melon Marshall Farrier's tech blog

Commentary, coding tips, libraries and utilities

There are two viable design patterns for writing object-oriented JavaScript. The first of these patterns, prototype chaining, is built into the language but has downsides. The second approach avoids these downsides by adding additional library code and is exemplified by base2. I've been using a similar, but far more lightweight piece of code (only 40 lines of code and 2kb unminified) that improves on the snippet that John Resig presents in his recent book Secrets of the JavaScript Ninja. I'll briefly talk about the first pattern before discussing my version of "roll your own" JavaScript inheritance.

Inheritance through prototype chaining

Consider the following example of prototype chaining:

function Animal() {
    this.breathe = function() {
        console.log('Breathing');
    };
};

function Mammal() {
    this.run = function() {
        console.log('Running');
    };
};

Mammal.prototype = new Animal();

function Bat() {
    this.fly = function() {
        console.log('Flying');
    };
    this.run = function() {
        console.log('cannot run');
    };
};

Bat.prototype = new Mammal();

var bat = new Bat();
bat.breathe(); // output: Breathing
bat.run(); // output: cannot run
bat.fly(); // output: Flying
if (bat instanceof Bat) console.log('Bat'); // output: Bat 
if (bat instanceof Mammal) console.log('Mammal');  // output: Mammal
if (bat instanceof Animal) console.log('Animal');  // output: Animal

Instances of the class Bat inherit, as expected, the functionality of superclasses, but this technique also has some downsides:

  • Confusion can arise because a constructor is indistinguishable from any other function.
  • 2 steps are required to implement the inheritance.
  • There is no way to directly reference superclass methods.

Any JavaScript function can in fact be used as a constructor, although the result will certainly be useless if the function hasn't been built for that purpose. Code becomes easier to read, however, if you can immediately distinguish between the declaration of a function intended to be used functionally and a function intended to be used as a constructor.

Secondly, there's no syntax for specifying the inheritance hierarchy when you declare the constructor for a derived class. You have to make that declaration in a separate place (lines 13 and 24 above), and the declaration is itself prone to the error of forgetting the new operator when you create a subclass. It's actually kind of counter-intuitive that it is not the class "Mammal" but rather one specific instance of that class that is going to be the prototype for the derived class. But if you leave the new out, your derived classes won't behave as expected.

Object-oriented programmers coming from Java or C++ would also expect to be able to call superclass methods, which often need to be modified in subclasses. For example, we might want to have a Dolphin subclass of Mammal that needs to come up for air before breathing. So, it would be nice to have some kind of syntax like:

function Dolphin() {
    this.breathe = function() {
        console.log("Surfacing");
        super.breathe();
    };
};

Dolphin.prototype = new Mammal();

Customized inheritance

We can resolve these difficulties with a custom implementation of inheritance:

var Extend = {},
    initializing = false;

// generic base class
Extend.Base = function() {};

Extend.Base.extend = function extend(properties) {
    var _super = this.prototype,
        proto,
        name;

    initializing = true;
    proto = new this();
    initializing = false;

    // leaving out check for presence of "_super" string
    for (name in properties) {
        proto[name] = typeof properties[name] === "function" &&
            typeof _super[name] === "function" ? (function (name, fn) {
                var retFn = function () {
                    var tmp = this._super,
                        ret;

                    this._super = _super[name];
                    ret = fn.apply(this, arguments);
                    this._super = tmp;
                    return ret;
                };
                return retFn;
            })(name, properties[name]) : properties[name];
    }

    function Class() {
        if (!initializing && this.init) {
            this.init.apply(this, arguments);
        }
    }

    Class.prototype = proto;
    Class.constructor = Class;
    Class.extend = extend;
    return Class;
};

The code is very similar to what John Resig proposes in Secrets of the JavaScript Ninja, pp. 145f., but with 3 improvements:

  • Rather than modifying JavaScript's native Object, this technique creates a separate Extend object with the desired functionality.
  • Resig's use of arguments.callee is eliminated as this feature is deprecated in strict mode in the 5th edition of ECMAScript (ES5).
  • The check for function serialization, which is supported in all modern browsers, is removed. The check is expensive, and, although Resig says function serialization isn't supported universally, I have been unable to find any browser where it isn't supported (IE6 and mobile appear to support this feature). More details here.

The above code, wrapped in such a way that it can be run with or without require.js, then allows the following inheritance hierarchy:

var Animal = Extend.Base.extend({
    breathe: function() {
        console.log('Breathing');
    }
});

var Mammal = Animal.extend({
    run: function() {
        console.log('Running');
    }
});

var Dolphin = Mammal.extend({
    breathe: function() {
        console.log('Surfacing');
        this._super();
    },

    run: function() {
        console.log("Can't run");
    }
});

var flipper = new Dolphin();

flipper.breathe(); // output: Surfacing\ Breathing
flipper.run();  // output: Can't run
if (flipper instanceof Dolphin) { console.log('Dolphin'); } // ouput: Dolphin
if (flipper instanceof Mammal) { console.log('Mammal'); } // ouput: Mammal
if (flipper instanceof Animal) { console.log('Animal'); } // ouput: Animal

And that's the behavior we're looking for!

The complete extend.js code can be used without modification and with no dependencies either as a require.js module or as a standalone.

Marshall

Contact:
info@codemelon.com

Projects

Utilities

Downloads

Documentation