Javascript Inheritance Part 3: Concatenation

This is part 3 of a four part series on Javascript inheritance.
Part 1: Overview and Use Case
Part 2: Delegation
Part 3: Concatenation
Part 4: Putting it All Together

Concatenation Implementation

Concatenation works by copying the methods from a prototypal to the object that wants to inherit without reference to the prototypal. Delegation works by copying just a reference to the prototypals via a call stack, but concatenation copies the methods over and attaches them to the object directly. Concentation has the following properties:

  1. Like delegation, methods also only need to be defined once. However, because child objects copy the method call stack into their own properties in concentation, calling the inherit function Concatenate() requires more time and it takes more memory (multiple referneces as entries in object's keys for each method vs a single reference __proto__).
  2. Unlike delegation, method calls resolve faster since you don't have to crawl the __proto__ call stack in search of method for parents.
  3. Like with proper delegation, with concatentation, the programmer controls which parent methods are used when method names conflict. In psuedo-class/constructor patterns, the hierarchy comes built in.
  4. Unlike delegation, because references methods are copied over to objects, any changes to parent prototypal method structures after object creation is not reflected in the 'child' object. For example, if Prototypal1 were to delete or assign a new function to .method1(), obj would still reference the original .method1() that it inherited when Concentate() was called on it.

Classical Mixin Pattern

The only difference between the delegation and the following pattern is under object creation:

var Prototypal1 = {
  delegatedVar1: 'Profane us ',
  delegatedVar2: 'in this',
  method1: function() {
    console.log(Prototypal1.delegatedVar1 + Prototypal1.delegatedVar2);
  },
  method2: function() {
    Prototypal1.method1(Prototypal1.delegatedVar1);
    console.log('refrain');
  }
};

var Prototypal2 = {
  delegatedVar1: 'Come what may',
  delegatedVar3: 'Say goodbye',

  method1: function() {
    console.log(Prototypal2.delegatedVar1);
  },
  method3: function() {
    Prototypal2.method1();
    console.log(Prototypal2.delegatedVar3);
  },
};

// Object creation
var obj = {};
Object.assign(obj, Prototypal1, Prototypal2);

// Examples
obj.method1(); // 'Come what may'
obj.method2(); // 'Profane us in this', 'refrain'
obj.method3(); // 'Come what may', 'Say goodbye'
console.log(obj.delegatedVar1); // 'Come what may'
console.log(obj.delegatedVar2); // 'in this'
console.log(obj.delegatedVar3); // 'Say goodbye'

The mixin definitions are the same as they were in the delegation pattern example however how methods are composed together onto the object is different. The main issue with this, as in the constructor and delegation patterns, is that you cannot get private variables.

Blueprint Mixin Pattern

var Prototypal1 = function (privilegedVariables) {
  var mixin = {};
  var privateVariable1 = 'hello';

  mixin.method1 = function () {
    console.log(privateVariable1);
  };
  
  mixin.method2 = function () {
    this.method1();
    privilegedVariables.share = 'I came from Prototypal1';
    console.log('goodbye');
  };
  
  return mixin;
};

var Prototypal2 = function (privilegedVariables) {
  var mixin = {};
  var privateVariable1 = 'Cat';

  mixin.method1 = function () {
    console.log(privateVariable1);
  };

  mixin.method3 = function () {
    this.method1();
    console.log(privilegedVariables.shared);
    console.log('Dog');
  };
  return mixin;
}

function Concatenate(obj) {
  // Skip over obj, getting the other parameters
  var prototypes = Array.prototype.slice.apply(arguments, 1);
  var privilegedVariables = {};

  // Inherit all the methods from the mixins
  prototypes.map(function (mixin) {
    Object.assign(obj, mixin(privilegedVariables)); // .extend() found in libaries would replace this
  });

  return obj;
}

var obj = {
  prop1: '',
  prop2: 5,
};
Concatenate(obj, Prototypal1, Prototypal2);

obj.method1(); // 'Cat'
obj.method2(); // 'hello', 'goodbye'
obj.method3(); // 'Cat', 'I came from Prototype1', 'Dog'

// Object assign is also called .extend() on several libraries
// It copies properties from source object
// The real object.assign of course has more logic behind it
// but according to John-David Dalton, the native Object.assign is very slow on most browsers
// so you may want consider implementing it on your own or using a library
Object.assign = function (obj, source) {
  var len = arguments.length;
  for (var i = 1; i < len; ++i) {
    var proto = arguments[i];
    proto.keys().map(function (key) {
      obj[key] = proto[key];
    });
  }
}
Thus, methods that are passed farther down the list of parameters in Object.assign() or Concatenate() in this case replace parameters. You'll find this pattern actually to be very similar to the constructor pattern. In fact if you were to try incoporate private variables into the constructor pattern as I described earlier, you'd get something very similar to this. Also lookout for factory functions as the prototypal complement for constructors in part 4 of this series.

Douglas Crockford calls this 'parasitic inheritence' (also here), a subset of something Crockford calls 'functional inheritance' in his book called Javascript: The Good Parts. Eric Elliot calls this pattern 'closure prototypes'. Angus Croll calls them 'functional mixins'. Aadit M. Shah, and what I ultimately decided to go with, calls them 'blueprints for mixins'. Particuarly of note is that Angus Croll's implementation allows you to circumvent using Object.assign() or an .extend() function.

In order to get private variables, you have to sacrifice memory to redefine methods for every instance of an object. A way to reduce this memory consumption is 1) to sacrifice private variables and mark with naming conventions, 2) via a weakmap or similar data structure, or 3) via partial functions that can be achieved by .bind()'ing the private varible to each method or making wrapping methods in functions.

// 1. Naming Conventions 
var Mixin = {
  _doNotUseMe: '',
  _privateVariable2: '',
  method1: function () {}
};

// 2. Weakmap or anytype of Hash, essentially a metadata approach
/** @namespace */
var Mixin1;
(function () {
  var private = new WeakMap();
  
  Mixin2 = {
    method1: function () {
      privateVars = private.get(this); // {this} would refer to the object mixin1 is attached to
      privateVars.hide = 5;
    }
  }; 
}());

var Mixin2;
(function () {
  var private = new WeakMap();
  
  Mixin2 = {
    method1: function () {
      privateVars = private.get(this);
    }
  }; 
}());

// 3. Partial Functions
Mixin2 = {
  method1: function (private, privilaged, ...actualArgs) {},
  method2: function (private, privilaged, ...actualArgs) {}
};

function Concatenate(obj) {
  var prototypes = Array.prototype.slice.apply(arguments, 1);
  var privilegedVariables = {};
  prototypes.map(function (mixin) {
    mixin.keys().map(function (method) {
      //obj[method] = mixin[method].bind(obj, private, privileged); // If your mixins use this, which they don't need to
      obj[method] = mixin[method].bind(null, private, privileged);
    });
  });
}

No comments:

Post a Comment

Note: only a member of this blog may post a comment.