9
\$\begingroup\$

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

  • Works in safe mode
  • Auto-calling of parent constructors
  • Makes sure that a constructor is only ever in the inheritance chain once
  • Support for multiple inheritance
  • Copying of class-wide functions over from parent 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 worms when dealing with JavaScript, which has a linear prototype chain.

My solution to the problem was simple: when inheriting from multiple classes, I actually create a new prototype chain made up of clones of every prototype (even the ones deep in) of the constructor you want to inherit from, avoiding duplication (see the documentation for more info).

This is a method I haven't seen anywhere else (which kind of worries me). I worked on it for the last five days nights (literally) and... well, it all seems to work. UPDATE: I think ringJs does something similar, although its code is a little hard to read

Notes:

  • The implementation of this.inherited() is interesting. Basically, it looks for the function in the prototype chain, and once found it looks up the method starting from that prototype object. I haven't seen this done anywhere else, but it's the only way I can assume ECMA 6 will do this

  • There is a bit of META data: when inheriting from multiple classes, each "cloned" one keeps a link to the original one (in the OriginalConstructor attribute of the constructor itself), in order to make , object.instanceOf() possible ; also, the actual constructor function defined by the user (to initialise values etc.) is stored in the .ActualCtor attribute of the constructor itself.

  • 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 FULL documentation here.

Questions:

  1. Did I do something obviously stupid?
  2. Can you find cases where this mechanism will break down?
  3. Comments on the "automatic parent calling" of constructors?
  4. What do you think about inherited()? Is it fast enough? (The penalty shouldn't be too much)
  5. Any performance disasters I am missing?
  6. What do you think about the ReturnedCtor() function?
  7. REMEMBERING that you cannot modify a method itself nor its attributes (since methods are actually shared), how would you memoize getObjectBase()?
  8. Any other comments?

"use strict";
/*
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.
*/

(function ( define ){

  define( function( require, exports, module ){

    function getObjectBase( o, fn ){

      var found = false, currentPoint = o;
      var k, i, l;

      while( currentPoint ){

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

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

      // If nothing was found, return null
      return { base: currentPoint, key: k };
    }

    // This will become a method call, so `this` is the object
    var getInherited = function( fn ){

      // Get the object's base 
      var objectBase = getObjectBase( this, fn );

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

      // 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`
      var p = Object.getPrototypeOf( objectBase.base );
      return p[ objectBase.key ]; 
    };


    var makeInheritedFunction = function( type ){

      // This will become a method call, so `this` is the object
      return function( fn, args, cb ){

        // Get the inherited function
        var fnUp = this.getInherited( fn );

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

        // Call the function. It could be sync or async
        if( type == 'async' ){
          var argsMinusCallback = Array.prototype.slice.call(args, 0, -1 ).concat( cb );
          return fnUp.apply( this, argsMinusCallback );
        } else {
          return fnUp.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 ) ){  
        for( var i = 0, l = SuperCtor.length; i < l; i ++ ) finalSuperCtorArray.push( SuperCtor[ i ] ); 
      } 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.OriginalConstructor ? Ctor.OriginalConstructor.prototype : Ctor.prototype;
      var current = this;
      var compare;

      while( ( current = Object.getPrototypeOf( current ) ) ){

        // 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 ){


      // Third implementation. The holy grail?
      var ReturnedCtor = function(){

        // The object's main constructor is being run. It will be responsible of
        // running all of the constructors in the prototype chain, starting from
        // the innermost and moving all the way out, except the last one
        // (which it itself) 
        if( Object.getPrototypeOf( this ).constructor === ReturnedCtor ){

          // Goes through the prototype chain and execute every single constructor.
          // If the constructor has the ActualConstructor attribute, then it's a SimpleDeclare
          // constructor 
          var l = [];
          var o = this;
          while( ( o = Object.getPrototypeOf( o ) ) ){
            l.push( o.constructor );
          }
          for( var i = l.length - 1; i >=1; i -- ){
            l[ i ].apply( this, arguments );
          }
          // Itself. Since I *know* this is a SimpleDeclare constructor,
          // run ActualConstructor if available
          var p = Object.getPrototypeOf( this );
          if( p.constructor.hasOwnProperty( 'ActualConstructor' ) ){
            p.constructor.ActualConstructor.apply( this, arguments );
          }

        // This is not the main object's constructor method -- plus, it's OBVIOUSLY
        // a SimpleDeclare constructor. Simply run the ActualConstructor if available
        } else {

          if( ReturnedCtor.hasOwnProperty( 'ActualConstructor' ) ){
            ReturnedCtor.ActualConstructor.apply( this, arguments );
          }

        }
      };

      /*
      // Second implementation. I don't like the lame way it checks if it's within ActualConstructor. Plus,
      // BIG problem: it only differentiates SimpleDeclare methods and Non-SimpleDeclare methods.
      var ReturnedCtor = function(){

        // Goes through the prototype chain and execute every single constructor.
        // If the constructor has the ActualConstructor attribute, then it's a SimpleDeclare
        // constructor 
        var l = [];
        var o = this;
        while( o = o.__proto__ ){
          // If it's a SimpleDeclare constructor, add the ActualConstructor instead
          if( o.constructor.toString() === ReturnedCtor.toString() ){
            if( o.constructor.ActualConstructor ) l.push( o.constructor.ActualConstructor );
          }
          // Otherwise, add the constructor
          else l.push(  o.constructor );
        }
        for( var i = l.length - 1; i >=0; i -- ){
          l[ i ].apply( this, arguments );
        }

      };
      */

       /*
      // First implementation. Scrapped because other constructors might break the chain
      // 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 !== Object ){
          ReturnedCtor.prototype.__proto__.constructor.apply( this, arguments );
        }

        // The stock constructor will simply run `ActualConstructor` if it's found.
        if( ReturnedCtor.hasOwnProperty( 'ActualConstructor' ) ){
          ReturnedCtor.ActualConstructor.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.ActualConstructor.
      // 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 ]; 
      }

      // We are not cloning a constructor, but creating a brand new one (using protoMixin as
      // a source of methods that just got added to the prototype).
      // ActualConstructor will be set to the `constructor` property of protoMixin
      if( ! SourceOfProto ){
        if( protoMixin.hasOwnProperty( 'constructor' ) ) ReturnedCtor.ActualConstructor = protoMixin.constructor;
      }

      // We are un the process of cloning an existing constructor.
      // When doing that:
      // * ReturnedCtor's ActualConstructor will be set to the Source's ActualConstructor.
      //   This will ensure that the stock constructor (that just invoks ActualConstructor) works.
      // * ReturnedCtor's OriginalConstructor will be set to the Source's ActualConstructor (or the souce itself).
      //   This will ensure that we have a path to the actual constructor we actually cloned,
      //   so that instanceOf() will work (by checking ActualConstructor whenever possible)
      if( SourceOfProto ){
        if( SourceOfProto.hasOwnProperty('ActualConstructor') ) ReturnedCtor.ActualConstructor = SourceOfProto.ActualConstructor;
        ReturnedCtor.OriginalConstructor = SourceOfProto.OriginalConstructor || SourceOfProto;
      }

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

    var copyClassMethods = function( Source, Dest ){

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

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

          // This statement is there so that strict mode still works -- even accessing these
          // automatically like this will cause an error!
          if( property !== 'arguments' && property !== 'callee' && property !== 'caller' ){

            // It's one of the attributes' in Function()'s prototype: skip
            if( Function.prototype[ property ] === Source[ property ] || property === 'prototype' ) continue;
            // It's one of the attributes managed by simpleDeclare: skip
            if( [ 'ActualConstructor', 'extend', 'OriginalConstructor' ].indexOf( property ) !== -1 ) continue;
            Dest[ property ] = Source[ property ];
          }
        }
      }
    };


    // This method will be attached to `list` in `declare()`,
    // and it will be used to make sure that only fresh prototypes
    // are added
    var constructorAlreadyInList = function( Ctor, list ){

      var CtorConstructor = Ctor.OriginalConstructor ||  Ctor;
      var protoConstructor;

      var found = false;
      for( var i = 0, l = list.length; i < l; i ++ ){
        var proto = list[ i ];

        protoConstructor = proto.constructor.OriginalConstructor || proto.constructor;
        if( protoConstructor === CtorConstructor ){
          found = true;
          break;
        }
      }

      return found;    
    };

    var declare = function( SuperCtorList, protoMixin ){

      var MixedClass, ResultClass;
      var list = [];
      var i, l, ii, ll;
      var proto;

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

      // Normalise SuperCtor into an array
      if( SuperCtorList === null ) SuperCtorList = [];
      if( typeof( SuperCtorList ) === 'function' ) SuperCtorList = [ SuperCtorList ];


      // No parameters: inheriting from Object directly, no inheritance at all
      if( SuperCtorList.length === 0 ){
        MixedClass = Object;
      }
      // Only one parameter: straght single inheritance.
      else if( SuperCtorList.length === 1 ){
        MixedClass = SuperCtorList[ 0 ];

      // More than one parameter: multiple inheritance at work
      // MixedClass will end up being an artificially made constructor
      // where the prototype chain is the sum of _every_ prototype in
      // every element of SuperCtorList (taking out duplicates)
      } else {
        MixedClass = Object;

        // NOW:
        // Go through every __proto__ of every derivative class, and augment
        // MixedClass by inheriting from A COPY OF each one of them.

        list = [];
        for( i = 0, l = SuperCtorList.length; i < l; i ++ ){

          // Get the prototype list, in the right order
          // (the reversed discovery order)
          // The result will be placed in `subList`
          var subList = [];    
          proto = SuperCtorList[ i ].prototype;
          while( proto ){
            if( proto.constructor !== Object ) subList.push( proto );
            proto = Object.getPrototypeOf( proto );
          }
          subList = subList.reverse();

          // Add each element of sublist as long as it's not already in the main `list`
          for( ii = 0, ll = subList.length; ii < ll; ii ++ ){
            if( ! constructorAlreadyInList( subList[ ii ].constructor, list ) ) list.push( subList[ ii ] );
          }
        }

        // For each element in the prototype list that isn't Object(),
        // augment MixedClass with a copy of the new prototype
        for( ii = 0, ll = list.length; ii < ll; ii ++ ){
          proto = list[ ii ];

          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
          }
        }
      }

      // Finally, inherit from the MixedClass, and add
      // class methods over
      // MixedClass might be:
      // * Object (coming from no inheritance),
      // * SuperCtorList[0] (coming from single inheritance)
      // * A constructor with the appropriate prototype chain (multiple inheritance)
      ResultClass = makeConstructor( MixedClass, protoMixin );
      copyClassMethods( MixedClass, ResultClass );

      // Add getInherited, inherited() and inheritedAsync() to the prototype
      // (only if they are not already there)
      if( ! ResultClass.prototype.getInherited ) {
        ResultClass.prototype.getInherited = getInherited;
      }
      if( ! ResultClass.prototype.inherited ) {
        ResultClass.prototype.inherited = makeInheritedFunction( 'sync' );
      }
      if( ! ResultClass.prototype.inheritedAsync ) {  
        ResultClass.prototype.inheritedAsync = makeInheritedFunction( 'async' );
      }

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

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

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

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

    exports = module.exports = declare;

  });
}(
    typeof define == 'function' && define.amd
        ? define
        : function ( factory ) { factory( require, exports, module); }
));
\$\endgroup\$
4
  • \$\begingroup\$ Just for everybody to know: I probably posted the question too soon, and have edited the code a couple of times. However, it is now "final enough" (I am writing tests, and it's passing everything) and don't think I will change the code further \$\endgroup\$
    – Merc
    Jan 12, 2015 at 6:22
  • \$\begingroup\$ Thanks. However I am now 100% happy with the code and am starting to think that it's the strongest implementation of inheritance in JavaScript... But I would love some feedback. The closest thing to SimpleDeclare is Ring but SimpleDeclare seems to be far superior. \$\endgroup\$
    – Merc
    Jan 13, 2015 at 9:13
  • \$\begingroup\$ Now the question is: is there anybody out there crazy enough to review such a tricky piece of code? :D Well, I will write documentation and unit tests tomorrow. Maybe that will help too... but I would love to see this code reviewed! \$\endgroup\$
    – Merc
    Jan 13, 2015 at 14:35
  • \$\begingroup\$ I am leaving the question open, hoping that somebody will eventually chime in. The code is here, it's stable, and on GitHub. I would love to know if you can spot any problems I (and the tests) didn't spot. Thanks! \$\endgroup\$
    – Merc
    Jan 25, 2015 at 0:00

1 Answer 1

5
+25
\$\begingroup\$

Let's start with the basics, and work our way up:

  • "use NONstrict"; What is that?
  • You have places where you have ; but it's not needed (after if, after while), and some places where it is needed but it's not there (after var = function() statement).
  • If an element is the last in the array/object, don't add a , after it.
  • There are a few problems with your declare function:

    • FirstConstructor is never used
    • list, proto, ii and ll, ResultClass are declared more than once
    • MixedClass is an implied global since it was never defined with the var keyword.

All of the above were reported by my IDE as soon as I pasted your code. Consider using a linter to ensure high code quality.

A few other things I spot by reading:

  • Using __proto__ is dangerous. It's deprecated and should not be used anymore. A better way is to augment to the constructor's prototype.
  • To iterate an array, use arr.forEach(fn) instead of a for loop. Declarative FTW!
  • if (!something) { something = somethingElse; } can be shortened to something = something || somethingElse.
Note, I ran out of time, expect more review and answers to your questions
\$\endgroup\$
7
  • \$\begingroup\$ 1) "use NONstrict" is because I want this code eventually to be strict. Unfortunately I use arguments.callee (and I need to). 2) Semicolon, commas,variables: all fixed 3) __proto__: fixed. 4) something = something || somethingElse I already use this pattern. I apply it whenever I can. Have I missed it somewhere? \$\endgroup\$
    – Merc
    Jan 14, 2015 at 14:05
  • \$\begingroup\$ The bits I didn't do: forEach(). Two reasons: 1) Speed 2) I want this to work in browsers as well (I realise I need polyfills for Object.createObject and Object.getPrototypeOf, but they are easy enough. \$\endgroup\$
    – Merc
    Jan 14, 2015 at 14:07
  • \$\begingroup\$ And... sorry, the natural thing for me to do is fix the code as problems are pointed out. Not sure if it's good policy here though? \$\endgroup\$
    – Merc
    Jan 14, 2015 at 14:08
  • \$\begingroup\$ @Merc No it's not, read this answer for more information : meta.codereview.stackexchange.com/a/1765 \$\endgroup\$
    – Marc-Andre
    Jan 14, 2015 at 15:19
  • \$\begingroup\$ Oh dear... sorry. Not touching the code again, I swear. \$\endgroup\$
    – Merc
    Jan 14, 2015 at 15:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.