1
\$\begingroup\$

I needed JavaScript inheritance that offered calling of overridden methods (super calls).

Furthermore I needed a possibiliy to add behaviour to instances (mixins).

Mixins should not only be full classes including mentioned inheritance but also be able to override its instance' method.

Below is what I came up with. It does what I needed it to do. May I ask to have a look at the implementation and its possible shortcomings?

log = function( s ) {
var dn = document.getElementById( "output" );
dn.innerHTML = dn.innerHTML + "</br>" + s;
}
function Class() { }
Class.prototype.construct = function() {};
Class.create = function() {
// create new instance
var oClass = arguments[0];
var args = arguments[1];
var oInstance = new oClass( args );	
// in case of mixins add behaviour to instance
if( args && args.mixins ) {
	var c = args.mixins.length;
	// loop through mixins prototypes an attach
	for( var ix=0; ix<c; ix++ ) {
		var oMixin = args.mixins[ ix ].o.prototype;
		// set flag whether mixin has init function
		var bInit = false;
		for (var sMember in oMixin) {
			if( sMember == "_init" ) {
				// mixin has init fuction
				bInit = true;
			}
			else {
				var oMember = oMixin[sMember];                      
				if (oMember instanceof Function && 
				    oInstance[sMember] ) {
					// in case of overrides save mixee's orig 
					// method and add _mixee() to call it
					if( ! oInstance.lfnMixee ) {
						oInstance.lfnMixee = {};
						oInstance._mixee = function() {
							var that = arguments[0];
							var args = arguments[1];
							var sfn = arguments[2];
							return that.lfnMixee[ sfn ].apply( that, args );
						};
					}
					var fn = oInstance[ sMember ];
					oInstance.lfnMixee[sMember] = fn;
				}
				// augment mixin's members
				oInstance[ sMember ] = oMember;
			}
		}
		if( bInit ) {
			// call mixin's init function with passed on arguments
			var aConfig = args.mixins[ ix ].aConfig;
			oMixin._init.apply( oInstance, [ aConfig ] );
		}
	}
}
if( oInstance._init ) {
	// call init function if exists
	oInstance._init( args );
}
return oInstance;
};
Class.extend = function(o) {
var oNew = function() {
	if (arguments[ 0 ] !== Class) {
		// call constructor if not base 
		this.construct.apply( this, arguments ); 
	}
};
// get prototype and hook to parent
var oPrototype = new this(Class);
var oParent = this.prototype;
for (var sMember in o) {
	var oMember = o[sMember];                      
	if (oMember instanceof Function) {
		oMember.oParent = oParent;
		// in case we override add method
		// _parent() to call parent's function
		oPrototype._parent = function() {
			var that = arguments[0];
			var args = arguments[1];
			var sfn = arguments[2];
			return args.callee.oParent[ sfn ].apply( that, args )
		};
	}
// augment with members
oPrototype[sMember] = oMember;
}
// set prototype
oNew.prototype = oPrototype;
// and make it extendable
oNew.extend = this.extend;      
return oNew;
};

/* ****** TEST CODE ********* */

var Person = Class.extend({
name : null,
_init : function( a ) {
	this.name = a.name;
},
say : function( s ) {
	s = '('+this.name+') "' + s +'"';
	log( s );
} 
});

var Moderator = Person.extend({
_init : function( a ) {
	this._parent( this, arguments, '_init' );
	this.name = "[mod|" + this.name +"]";
},
say : function( s ) {
	log( 'as moderator I say: ' );
	this._parent( this, arguments, 'say' );
},
read : function( s ) {
	s = "..."+s;
	log( s );
} 
});

var Megaphone = Class.extend({
say : function( s ) {
	log( "&lt;YELL&gt;" );
	this._mixee( this, arguments, 'say' );
	log( "&lt;/YELL&gt;" );
}
});

var Googles = Class.extend({
type : "Googles",
color : "transparent",
read : function() {
	log( "&lt;SHARP&gt;" );
	this._mixee( this, arguments, 'read' );
	log( "&lt;/SHARP&gt;" );
},
clean : function(){
	log( this.name + " cleaning "+ this.color +" " + this.type );
}	
});

var Shades = Googles.extend({
type : "Shades",
_init : function( a ) {
	this.color = a.color;
},
read : function() {
	log( "&lt;"+this.color+"&gt;" );
	this._parent( this, arguments, 'read' );
	log( "&lt;/"+this.color+"&gt;" );
},
clean : function(){
	this._parent( this, arguments, 'clean' );
}	
});

var john = Class.create( Person, {
name : "john",
mixins: [ {
	o : Googles
} ]
 } );

var joseph = Class.create( Moderator, { 
name : "joseph",
mixins: [ {
	o : Shades,
	aConfig : { color : "blue" }
	}
	, { o : Megaphone }
]
 } );

john.say( 'hello' );
john.clean();

log( '---------' );

joseph.say( 'hello' );
joseph.read( 'agenda of meeting' );
joseph.clean();
<div id="output"></div>

\$\endgroup\$
1
\$\begingroup\$

ES6 has support for classes. It also has support for super calls and mixins. I highly recommend you ditch this approach and write future-proof code. You can always use a transpiler like Babel to convert ES6 code back to ES5-compliant code which most, if not all, browsers are supporting.

log = function( s ) {

There's no var keyword. Should you want to wrap this in a closure, this will shoot log back up the global scope.

var c = args.mixins.length;
// loop through mixins prototypes an attach
for( var ix=0; ix<c; ix++ ) {

mixins appear to be an array. Use forEach instead of a for loop. This way, you avoid creating the extra index variables. forEach provides its callback with the index of the item as the second argument.

for (var sMember in oMixin) {

oMixin appears to be an object. Suggesting you use Object.keys() in combination with forEach to loop through objects. Object.keys() provides the own enumerable keys like a for-in loop with a hasOwnProperty guard.

oMember instanceof Function

Consider using typeof something === 'function'. typeof is preferred over instanceof since you're just dealing with basic types, not custom instances of constructors. More on that in this SO comment.

Lastly, prefer composition (augmenting of objects) over inheritance (subclassing). I personally don't care which one's The Right Way™ since I work with projects that do either pretty well. However, inheritance forces you to create a taxonomy of things and assume you can foresee the future of your app.

But if something unexpected happens, like having some class that doesn't follow the taxonomy or wants to have functionality that exists in a separate branch of the hierarchy, you eventually resort to either duplication of code, or multiple inheritance. This makes your code, your architecture and the system that enables you to do it (ie: your code up there) to become really hairy. TL;DR: Keep it simple.

Here's an example of augmentation using Object.assign.

// Creates a person object.
function createPerson(){
  return {
    name: 'person',
    say: function(message){ alert(message); }
  };
}

// Create a moderator person that "inherits" a person. Essentially we just
// created an augmented version of the person, hijacking the say function.
function createModerator(){
  var subject = createPerson();
  return Object.assign({
    ...subject,
    say: function(message){
      subject.say(`moderator:${message}`);
    }
  });
}

// Example of a mixin. We augment the subject with a `say` function that
// also modifies the function. The difference with this and above is that
// the mixin accepts a subject dynamically while the above knows about
// the subject in advance, like you would in inheritance and mixins.
function megaphoneMixin(subject){
  return Object.assign({
    ...subject,
    say: function(message){
      subject.say(message.toUpperCase());
    }
  });
}

var normalPerson = createPerson();
var moderatorPerson = createModerator();
var moderatorWithMegaphones = megaphoneMixin(createModerator());

normalPerson.say('hello');
moderatorPerson.say('hello');
moderatorWithMegaphones .say('hello');
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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