Skip to main content
3 of 15
added 2 characters in body
Jamal
  • 35.2k
  • 13
  • 134
  • 238

Multiple inheritance with Javascript with support of calling of super() methods

I have just finished working on a Node.JS module that provides "multiple inheritance that works". I had some really specific requirements:

  • Support for defining constructors
  • Auto-calling of parent constructors
  • Support for multiple inheritance
  • Copying of class-wide functions over from father to child
  • Ability to run this.inherited(arguments) from a method which would result on calling the method of a parent class

The "support for multiple inheritance" was the real sticky one. Multiple inheritance opens a huge can of works when dealing with JavaScript, which has a linear prototype chain.

My solution to the problem was simple: when inheriting from multiple classes, only the first one would be "real". All of the others would be clones of the passed constructor. I made sure I cloned everything -- not just the constructor itself, but the whole chain.

This is a method I haven't seen anywhere else (which kind of worries me). I worked on it for the last three days nights (literally) and... well, it all seems to work.

Notes:

  • The implementation of this.inherited() is interesting. Basically, it looks for the function in the prototype chain, and then it keeps on going "down" the chain till it finds another matching key. I haven't seen this done anywhere else, but it's the only way I can assume ECMA 6 will do thi

  • There is a bit of META data: when inheriting from multiple classes, each "cloned" one keeps a link to the original one; also, the actual constructor function is stored in the .ActualCtor attribute in the constructor itself, which makes instanceOf possible

  • The code is on GitHub too -- the code there has a lot of examples that test the thing. I will make sure I have tests written early next week. There is also some basic documentation here.

Questions:

  1. Did I do something obviously stupid?
  2. Do you think this mechanism will break down?
  3. Comments on the "automatic parent calling" of constructors?
  4. What do you think about inherited()?
  5. Any performance disasters I am missing?
  6. REMEMBERING that you cannot modify a method itself nor its attributes (since methods are actually shared), how would you memoize inherited()?
  7. Any other comments?
/*
Copyright (C) 2015 Tony Mobily

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

// Note to self: I didn't think I would ever end up writing
// something like this. I wish super() was implemented in node.
// We will need to wait for ECMA 6... Ugh.
var inherited = function( type, args, cb ){

  // The real function that called this.inherited() is actually two levels up...
  var f = arguments.callee.caller.caller;

  // Look for the function itself in the object's prototypal hiherarchy 
  var currentPoint = this;
  var found;

  found = false;
  while( currentPoint ){

    var objMethods = Object.getOwnPropertyNames( currentPoint );
    for( var i = 0, l = objMethods.length; i < l; i ++ ){
      var k = objMethods[ i ];

      if( currentPoint.hasOwnProperty( k ) && currentPoint[ k ] === f ){
        found = true;
        break;
      }
    };
    // If found, break out of the cycle. Otherwise, keep looking in the next proto up
    if( found ) break;
    currentPoint = currentPoint.__proto__;
  }

  // If the function is not found anywhere in the prototype chain
  // there is a pretty big problem
  if( ! found ) throw new Error( "inherited coun't find method in chain -- "  + type );

  // At this point, I know the key. To look for the super method, I
  // only have to check if one of the parent __proto__ has a matching key `k`

  currentPoint = currentPoint.__proto__; // Starting from the one up
  found = false;
  while( currentPoint ){
    if( currentPoint.hasOwnProperty( k ) ){
      found = true;
      break;
    }
    currentPoint = currentPoint.__proto__;
  }

  // No inherited element in the chain, just call the callback (async) or return nothing
  if( ! found  ){
    if( type === 'async' ) return cb.call( this, null );            
    if( type === 'sync' ) return;
  }

  var fn = currentPoint[ k ];

  // Call the function. It could be sync or async
  if( type == 'async' ){
    var argsMinusCallback = Array.prototype.slice.call(args, 0, -1 ).concat( cb )
    return fn.apply( this, argsMinusCallback );
  } else {
    return fn.apply( this, args );
  }
};


// This will be added as a Constructor-wide method
// of constructor created with simpleDeclare (only if needed)
var extend = function( SuperCtor, protoMixin ){

  // Only one argument was passed and it's an object: it's protoMixin.
  // So, just return declare with `this` as base class and protoMixin
  if( arguments.length === 1 ){

    if( typeof( SuperCtor ) === 'object' && SuperCtor !== null ) return declare( [ this ], SuperCtor );
    else protoMixin = {};
  }
  
  // SuperCtor is is either a constructor function, or an array of constructor functions
  // Make up finalSuperCtorArray according to it.
  var finalSuperCtorArray = [ this ];  
  if( Array.isArray( SuperCtor ) ){  
    SuperCtor.forEach( function( Ctor ){ finalSuperCtorArray.push( Ctor ); } );
  } else if( typeof( SuperCtor ) === 'function' ) {
    finalSuperCtorArray.push( SuperCtor );
  } else {
    throw new Error( "SuperCtor parameter illegal in declare (via extend)");
  }

  return declare( finalSuperCtorArray, protoMixin );
}


// Look for Ctor.prototype anywhere in the __proto__ chain.
// Unlike Javascript's plain instanceof, this method attempts
// to compare 
var instanceOf = function( Ctor ){

  var searchedProto = Ctor.prototype;
  var current = this;
  var compare;

  while( current = current.__proto__){

    // It will compare either with originalConstructor.prototype or plain prototype
    compare = current.constructor.originalConstructor ?
              current.constructor.originalConstructor.prototype :
              current.constructor.prototype;         

    // Actually run the comparison
    if( compare === searchedProto ) return true;
  }
  return false;
}
  
var makeConstructor = function( FromCtor, protoMixin, SourceOfProto ){

  // The constructor that will get returned. It's basically a function
  // that calls the parent's constructor and then protoMixin.constructor.
  // It works with plain JS constructor functions (as long as they have,
  //  as they SHOULD, `prototype.constructor` set)
  var ReturnedCtor = function(){

    // Run the parent's constructor if present
    if( ReturnedCtor.prototype.__proto__ && ReturnedCtor.prototype.__proto__.constructor ){
      ReturnedCtor.prototype.__proto__.constructor.apply( this, arguments );
    }

    // If an actual constructor is set, it was placed in ReturnedCtor.ActialCtor.
    // If there is one, run it
    if( ReturnedCtor.hasOwnProperty( 'ActualCtor' ) ){
      ReturnedCtor.ActualCtor.apply( this, arguments );
    }
  
  };

  if( protoMixin === null ) protoMixin = {};
  if( typeof( protoMixin ) !== 'object' ) protoMixin = {};
  
  // Create the new function's prototype. It's a new object, which happens to
  // have its own prototype (__proto__) set as the superclass' prototype and the
  // `constructor` attribute set as FromCtor (the one we are about to return)

  ReturnedCtor.prototype = Object.create(FromCtor.prototype, {
    constructor: {
      value: ReturnedCtor,
      enumerable: false,
      writable: true,
      configurable: true
    },
  });

  // Copy every element in protoMixin into the prototype.
  // Note that `constructor` is special: it's _not_ copied over.
  // Instead, it's placed in ReturnedCtor.ActualCtor.
  // It can either come:
  //   * from protoMixin, in cases where SourceOfProto is not defined
  //     (which means that it's what the developer passed herself in `protoMixin` as `constructor`)
  //   * from the source of protoMixin, in cases where SourceOfProto is defined
  //     (which means that we are taking it from the SourceOfProto, since the goal
  //      is to mimic it completely creating a working copy of the original constructor)

  var ownProps = Object.getOwnPropertyNames( protoMixin );
  for( var i = 0, l = ownProps.length; i < l; i ++ ){
    var k = ownProps[ i ];

    if( k !== 'constructor' ) ReturnedCtor.prototype[ k ] = protoMixin[ k ];

    // ActualCtor comes from what the user placed as `constructor` as the second
    // parameter of `declare()`
    if( ! SourceOfProto && protoMixin.hasOwnProperty( 'constructor' ) ){
      ReturnedCtor.ActualCtor = protoMixin.constructor;
    }
  };
  // ActualCtor comes from the SourceOfProto, the constructor function we are
  // emulating
  // Basically if the parent class has `ActualCtor`, then the cloned one will also
  // definitely need it as the stock ReturnedCtor will look for it
  if( SourceOfProto ){
    if( SourceOfProto.hasOwnProperty('ActualCtor') ) ReturnedCtor.ActualCtor = SourceOfProto.ActualCtor;
  }

  // That's it!
  return ReturnedCtor;
}

var copyClassMethods = function( Source, Dest ){

  // Copy class methods over
  if( Source !== null && Source !== Object ){

    var ownProps = Object.getOwnPropertyNames( Source );
    ownProps.forEach( function( property ) {
      // It's one of the attriutes' in Function()'s prototype: skip
      if( Function.prototype[ property ] === Source[ property ] || property === 'prototype' ) return;
      // It's one of the attributes managed by simpleDeclare: skip
      if( [ 'ActualCtor', 'extend', 'originalConstructor' ].indexOf( property ) !== -1 ) return;
      Dest[ property ] = Source[ property ];
    });
  }
}

var declare = function( SuperCtorList, protoMixin ){

  var ResultClass;

  // Check that SuperCtorList is the right type
  if( SuperCtorList !== null && typeof( SuperCtorList ) !== 'function' && !Array.isArray( SuperCtorList ) ){
    throw new Error( "SuperCtor parameter illegal in declare");
  }

  // SuperCtor is null: the array will be an empty list
  if( SuperCtorList === null ) SuperCtorList = [];
  if( ! Array.isArray( SuperCtorList ) ) SuperCtorList = [ SuperCtorList ];

  // Set the starting point. It's either the first SuperCtor, or
  // Object
  var MixedClass = SuperCtorList[ 0 ] || Object;


  // Enrich MixedClass inheriting from itself, adding SuperCtor.prototype and
  // adding class methods

  // The class is inheriting from more than one class: it will go through
  // every __proto__ of every derivative class, and will augment MixedClass by
  // inheriting from each one of them
  for( var i = 1, l = SuperCtorList.length; i < l; i ++ ){
    var proto;

    proto = SuperCtorList[ i ].prototype;
    var list = [];
    while( proto ){
      list.push( proto );
      proto = proto.__proto__;
    };
    list.reverse().forEach( function( proto ){

      var M = MixedClass;

      if( proto.constructor !== Object ){

        MixedClass = makeConstructor( MixedClass, proto, proto.constructor );    

        copyClassMethods( M, MixedClass ); // Methods previously inherited
        copyClassMethods( proto.constructor, MixedClass ); // Extra methods from the father constructor

        // This will make this.instanceOf() work, and it will give us a link
        // to the constructor that actually originated this copy.
        // Note that copies of copies will still retain the original one
        MixedClass.originalConstructor = proto.constructor.hasOwnProperty( 'originalConstructor' ) ? proto.constructor.originalConstructor : proto.constructor;
      }
    })
  };

  // Finally, inherit from the MixedClass, and add
  // class methods over
  var ResultClass = makeConstructor( MixedClass, protoMixin );

  copyClassMethods( MixedClass, ResultClass );
  ResultClass.originalConstructor = ResultClass; // This will make this.instanceOf() work

  // Add inherited() and inheritedAsync() to the prototype
  // (only if they are not already there)
  if( ! ResultClass.prototype.inherited ) {
    ResultClass.prototype.inherited = function( args ){
      return inherited.call( this, 'sync', args );
    }
  }
  if( ! ResultClass.prototype.inheritedAsync ) {  
    ResultClass.prototype.inheritedAsync = function( args, cb ){
      return inherited.call( this, 'async', args, cb );
    }
  }

  // Add instanceOf
  if( ! ResultClass.prototype.instanceOf ) {    
    ResultClass.prototype.instanceOf = instanceOf;
  }

  // Add class-wide method `extend`
  ResultClass.extend = function( SuperCtor, protoMixin ){
    return extend.apply( this, arguments );
  }

  // That's it!
  return ResultClass;
};


// Returned extra: declarableObject
declare.extendableObject = declare( null );

exports = module.exports = declare;
Merc
  • 531
  • 2
  • 11