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:
- Did I do something obviously stupid?
- Can you find cases where this mechanism will break down?
- Comments on the "automatic parent calling" of constructors?
- What do you think about
inherited()
? Is it fast enough? (The penalty shouldn't be too much) - Any performance disasters I am missing?
- What do you think about the
ReturnedCtor()
function? - REMEMBERING that you cannot modify a method itself nor its attributes (since methods are actually shared), how would you memoize
getObjectBase()
? - 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); }
));