JavaScript classes and inheritance (part II) post

01/03/2015 with tags : javascript

In my last post I was playing around with JavaScript, trying to create something that could mimic classic concept of a class. The idea behind it was to create a structure that could be then extended and will handle method overriding.

Inheritance pitfalls

Inheritance described above is not working corectly in all cases.

It will not work correctly when calling overriden method from class that is not defining the overriding method. Example below shows a simple inheritance chain

   var A = Class.extend({
       construct: function() {
           this.value = 1;
           console.log(">> A construct");
       },
       inc: function() {
           console.log(">> A inc");
           this.value += 1;
       }
   });

   var B = A.extend({
       construct: function() {
           // `class` A construct method will be called here
           console.log(">> B construct");
       },
       inc: function() {
           this._super('inc');
           console.log(">> B inc");
           this.value += 1;
       }
   });

   var C = B.extend({
       construct: function() {
           // `class` A construct method will be called here
           console.log(">> C construct");
       }
   });

   var c = new C(); // >> A construct \n >> B construct
   c.inc();
   console.log(c.value); // expected result is 3

Problem here is related with fact, that each 'class' keeps track of its own 'superclass'.

Inheritance is done using prototype chain, this means that if a class is not defining a called method directly it is being searched in a prototype chain. In above example method inc() is called on an instance of class C. Method is not defined by class C but is defined by one of the classed in prototype chain - class B. In that case method B.inc() is called but in the context of a C class instance (this point to an instance of class C). This means that although we are executing code from class B, internal pointer for calling super methods points also to class B. This is a base class for class C and the code is executed in context of an C class instance.

Solution

I have found a great solution while reviewing fabric.js code.

The idea is to create a special function for hadling method overriding and capture a super class pointer as a scope variable. Then we can temporary modify super class pointer and force it to point to a proper super class before _super(...) is called.

 ...
 var propValue = instanceProps[property];
 var hasSuperCall = _isF(propValue) && ((propValue + '').indexOf('_super') != -1);
 if( hasSuperCall && (property in childProto) && _isF(childProto[property]) )
 {
    childProto[property] = (function(property){ 
        return function() {
            var __super__ = this.__super__;
            this.__super__ = parent.prototype;
            var result = instanceProps[property].apply(this, arguments);
            this.__super__ = __super__;
            return result;
        }
    })(property);
}
 ...
 (_isF - checks if a parameter is function)

Class pattern

Putting this all together

 var DummyCtor = function(){};

 // base `class`
 var Class = function () {};

 // instance init method
 Class.prototype.construct = function() {};

 // a way to introduce method overriding - call super method
 Class.prototype._super = function(name,args) {
    var f = this.__super__[name];
    return f ? f.apply( this, _.isArray(args)?args:[args] ) : null;
 };

 // `extend` class
 Class.extend = function(instanceProps, staticProps) 
 {
        // `class` to extend
        var parent = this;

        // object instance init method - user provided or empty function
        var ctor = (instanceProps && instanceProps.hasOwnProperty('construct')) ? instanceProps['construct'] : DummyCtor;

        // `new class` constructor will call super `class` constructor
        var child = function(){
            parent.apply(this, arguments);
            ctor.apply(this, arguments);
        };

        // static methods
        _.extend(child, parent, staticProps);

        // Temporary constructor pattern (1*)
        // (naming borrowed from Backbone)
        var Surrogate = function(){};
        Surrogate.prototype = parent.prototype;

        var surrogate = new Surrogate();
        // keep track of super `class`
        surrogate.__super__ = parent.prototype;
        // update constructor pointer
        surrogate.constructor = child;
        // setup prototype chain
        child.prototype = surrogate;

        // new `class` instance methods
        if (instanceProps) 
        {
            var childProto = child.prototype;
            for (var property in instanceProps)
            {
                var propertyValue = instanceProps[property];

                // check if property is a function and if there is a '_super' call
                var hasSuperCall = _isF(propertyValue) && ((propertyValue + '').indexOf('_super') != -1);
                if( hasSuperCall && (property in childProto) && _isF(childProto[property]) )
                {
                    // capture 'property' and create method-overriding function
                    childProto[property] = (function(property){ 
                        return function() {
                            var __super__ = this.__super__;
                            this.__super__ = parent.prototype;
                            var returnValue = instanceProps[property].apply(this, arguments);
                            this.__super__ = __super__;
                            return returnValue;
                        }
                    })(property);
                }
                else
                {
                    childProto[property] = propertyValue;
                }
            }
        }

        return child;
    };