Search code examples
javascriptoopmixins

Javascript mixin pattern setting instance specific this variables


If I use constructor functions for my objects and prototype for shared functionality I would like to mixin shared functionality (functions) to the object's prototype but instance specific (this varaibles) to the object instances.

To add the prototype part I found this pattern. To set instance variables that are assumed to be there by the prototype functions I came up with an init (one for each mixin).

Here is a simple example:

var mixIn=function(target,source){
  for(fn in source){
    if(source.hasOwnProperty(fn)){
      target.prototype[fn]=source[fn];
    }
  }
};
var SpeakEnable = {
  say:function(){
    console.log(this.message);
  },
  initSpeak:function(){// for initializing instance vars
    this.message="Hello World Mixed in!";
    this.object=[];
  }
};
var Person=function(){
  this.initSpeak();//have to init instance vars
};
// set up inheritance
// set up Person.prototype
// set speak enable
mixIn(Person,SpeakEnable);

var lulu=new Person();
lulu.say();

var june=new Person();
console.log(june.say===lulu.say);//true
console.log(june.object===lulu.object);//false

This all works fine and dandy but initializing the instance variables is where I have some problem with. It somehow doesn't seem to be a very clean way. When I mix in several mixins the Person constructor function has to call all the init functions to set up the instance variables. Forgetting to call it will result in strange errors (in this case console logging undefined when say is called on an instance).

So the question is: is there a cleaner way to setup initial instance variables that are assumed to be there by the mixin functions?


Solution

  • You could inherit all mixable objects from a base object that ensures proper initialization. This is a clean way of achieving your goal.

    The following code demonstrates this principle:

    //------------ framework
    
    var inherits = function(childCtor, parentCtor) {
      function tempCtor() {};
      tempCtor.prototype = parentCtor.prototype;
      childCtor.superClass_ = parentCtor.prototype;
      childCtor.prototype = new tempCtor();
      childCtor.prototype.constructor = childCtor;
    };
    
    var mixIn=function(target,source){
      for(fn in source){
        if(source.hasOwnProperty(fn) && fn.name != 'init'){
          target.prototype[fn]=source[fn];
        }
      }
    
      if (typeof source.init == 'function') { 
          if (target.prototype._mixInits === undefined) { 
              target.prototype._mixInits = [];
          }
          target.prototype._mixInits.push(source.init);
      }
    };
    
    // all objects that can be mixin's should inherit from
    // this object in order to ensure proper initialization
    var Mixable = function() {
        var mixInits = this.__proto__._mixInits;
        if (mixInits !== undefined) {
            for (var i = 0; i < mixInits.length; i++) {
                mixInits[i].call(this);
            }
        }
    };
    
    //------------ testcode
    
    var SpeakEnable = {
      say:function(){
        console.log(this.message);
      },
      init:function(){
        console.log('say init called');
        this.message="Saying Hello World Mixed in!";
        this.object=[];
      }
    };
    
    var WalkEnable =  {
      walk:function(){
        console.log(this.walk_message);
      },
      init:function(){
        console.log('walk init called');
        this.walk_message="Walking step 1.2.3.";
      }
    };
    
    
    var Person=function() {
      Mixable.call(this);
    };
    
    inherits(Person, Mixable);
    
    mixIn(Person,SpeakEnable);
    mixIn(Person,WalkEnable);
    
    var lulu=new Person();
    lulu.say();
    lulu.walk();