JavaScript Class Inheritance Framework

In order to write elegant JS code, the first thing we needed was to set up a framework for object-oriented programming. A lot of what we need is already built into the JavaScript language, which is a fully object-oriented language. But we wanted to sidestep some of the pitfalls of standard inheritance patterns while adding some extra features.

We started with this class inheritance framework by John Resig, which gave us the ability to write beautiful code like this example from Resig:

var Person = Class.extend({
    init: function(isDancing){
        this.dancing = isDancing;
    },
    dance: function(){
        return this.dancing;
    }
});

var Ninja = Person.extend({
    init: function(){
        this._super( false );
    },
    dance: function(){
        // Call the inherited version of dance()
        return this._super();
    },
    swingSword: function(){
        return true;
    }
});

var p = new Person(true);
p.dance(); // => true

var n = new Ninja();
n.dance(); // => false
n.swingSword(); // => true

// Should all be true
p instanceof Person && p instanceof Class &&
n instanceof Ninja && n instanceof Person && n instanceof Class

Resig’s framework is great, but we wanted some additional features in our Class implementation:

  1. Magically have a _this variable that points to an object instance from all invocations of its class methods. This is to avoid writing “var _this = this” whenever we’re anticipating having to reference the object from inside a closure (which is pretty much all the time). Unfortunately, the only way to put a variable in the active context is either to define it from code located in lexical scoping range, or to put it in the global namespace. And so we had to mess around with window._this.
  2. Enable chaining method calls, e.g. “obj.set(‘property’, ‘value’).render()”. We did this by wrapping all object methods with code that looks at the original return value, and if it’s undefined, it gets changed to a pointer to the object instance. So if the original method M doesn’t have a return statement, you can chain its calls: “obj.M().N()”

Here is the code to our OOP framework, which you can also find at http://quixey.com/static/js/class.js:

/*
 * OOP framework for JavaScript
*/

(function(){
    var extending = false;

    // The base Class implementation (does nothing)
    this.Class = function(){};

    // Create a new Class that inherits from this Class
    Class.extend = function(props) {
        var _super = this.prototype;

        // Instantiate a parent class (but only create the instance,
        // don't run the init constructor)
        extending =  true;
        var prototype = new this();
        extending = false;

        // Copy the class props into the prototype on top of the parent class props
        for (var name in props) {
            prototype[name] = props[name];
        }

        // Class constructor
        var Class = function() {
            var _this = this;

            if (!extending) {
                // Return an initialized instance with wrapped functions

                for (var name in _this) {
                    if (typeof _this[name] == "function") {
                        // Wrap every function in the newly created instance
                        _this[name] = function(name, func) {
                            return function() {
                                // Store a reference to the source function
                                // for easy debugging
                                arguments.callee.source = func;

                                // Magically put _this and _super in the global context
                                // for the duration of the object method call
                                var old_this = window._this;
                                var old_super = window._super;
                                window._this = _this;
                                window._super = _super[name];

                                // Always call an instance method with the instance as the context
                                var ret = func.apply(_this, arguments);

                                // Restore the old values of _this and _super
                                window._this = old_this;
                                window._super = old_super;

                                // Make all instance methods return the instance by default.
                                // Enables method chaining, i.e. obj.method1().method2()
                                if (ret === undefined) {
                                    return _this;
                                } else {
                                    return ret;
                                }
                            };
                        }(name, _this[name]);
                    }
                }

                // Set up the expected constructor
                _this.constructor = Class;

                // All construction is done in the init method
                if (_this.init) {
                    _this.init.apply(_this, arguments);
                }
            }
        }

        // Populate our constructed prototype object
        Class.prototype = prototype;

        // And make this class extendable
        Class.extend = arguments.callee;

        return Class;
    };
})();
About these ads