Javascript Inheritance Part 2: Delegation

This is part 2 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

Prototypal Inheritance

There are two types of prototype-based inheritance: delegation (aka. differential) and concatenation (aka. cloning). And because Javascript allows for a lot of freedom, there are several design patterns to implement either type of prototypal inheritance, each with its own name. This combination of different inheritance paradigms, delegation vs concatenation, and implementations in combination with Javascript being the first to bring many of these concepts to the mainstream is what makes the terminology so confusing.



Delegation

This is the type of inheritance Javascript implements natively. Eg. The 'function' and 'Array' objects inherit from 'Object' object via delegation.
var arr = []; // Creates an object, assigns property arr.length = 0, sets __proto__ = Array.prototype

// Now let us make a call to illustrate delegation inheritance
arr.hasOwnProperty();
/**
 * 1. Searches the keys of arr for hasOwnProperty(), finds nothing, delegates to Array global
 * 2. Searches arr.__proto__ (which points to Array.prototype), cannot find. Delegates to Object global
 * 3. Searches arr.__proto__.__proto__ (which points to Object.prototype), finds.
 * 4. Execute arr.__proto__.__proto__.findOwnProperty();
 */

// You can test this by
console.log(arr.hasOwnProperty); // Outputs: function object named hasOwnProperty
arr.__proto__ = null;
console.log(arr.hasOwnProperty); // Outputs: undefined
Do note that although I define the object arr in this example, prototypal inheritance is separate from object declaration. The main properties of delegation are:
1. Methods only need to be defined once in memory. Inherited methods are found via the __proto__ call stack.
2. Methods from distant parent prototypes take longer to find.
3. Delegation references child methods first, which works in tandem with implementation details to solve problems of multiple inheritance.
4. Changes to methods of the parent prototype propagate immediately to children, since Javascript will crawl the __proto__ call stack.
It should be noted that for the context of the above points, properties and methods are essential synonymous in Javascript.

Implementations

There are several design patterns that can implement delegation. We'll start with the most familiar:

Constructor or Psuedo-Class Pattern

// ECMA 6 version
class ParentClass {
  constructor (param1) {
    this.publicVar1 = 'hello';
    this.publicVar2 = param1;
  }
  method1() {
    console.log(this.publicVar1 + this.publicVar2);
  }
  method2() {
    this.method1();
    console.log('beaver');
  }
}

class ChildClass extends ParentClass {
  constructor(param2) {
    super(param2);
    this.publicVar3 = 'the';
  }
  method1() {
    console.log(this.publicVar3);
  }
  method3() {
    this.method1();
    console.log('dog');
  }
}




// Pre-ES6 Pattern that the ES6 pattern evaluates to:
function ParentClass(param1) { // Construct
  this.publicVar1 = 'hello';
  this.publicVar2 = param1;
}

ParentClass.prototype.method1 = function () {
  console.log(this.publicVar1 + this.publicVar2);
};

ParentClass.prototype.method2 = function() {
  this.method1();
  console.log('beaver');
};

function ChildClass(param2) {
  ParentClass.call(this, param2); // Super(param2);
  this.publicVar3 = 'the';
}

ChildClass.prototype = Object.create(Hello.prototype); // Need to create an intermediary object for

ChildClass.prototype.method1 = function () {
  console.log(this.publicVar3);
};

ChildClass.method3() {
  this.method1();
  console.log('dog');
}



// Class instantiate
var instantiatedObject = new ChildClass('world');

// Examples
instantiatedObject.method1(); // 'the'                   // ChildClass.method1() found first
instantiatedObject.method2(); // 'hello world', 'beaver' // Retains ParentClass.method1()
instantiatedObject.method3(); // 'the', 'dog'


// The keyword new is just synatatic sugar (ie. a macro) for
var instantiatedObject = {};
instantiatedObject = ClassName.call(classInstance);
instantiatedObject.__proto__ = className.prototype;

The key to understanding the how the constructor pattern works is understanding how the this keyword works: new does not instantiate an object in Javascript as one might expect from other languages that use class-based inheritance, it creates an object and assigns the this of the 'constructor' function to said object.

The only way to get private variables through the ES6 class keyword or through the pre-ES6 constructor pattern is if you declare methods inside the constructor allowing you to make use of the closure of the constructor to define private variables to which methods have access. An example of this can be found here. Doing this with the class keyword defeats the whole point of using the class keyword as it requires you to know the internals of how it works. Doing this with the pre-ES6 implementation however is more acceptable, but still requires you to know how the implementation of new works, again defeating the point of using new in the first place. We'll get into the memory issue in the next part on concatenation.

A more faithful transpile from ES6 to ES5 can be found in this Babel output which also demonstrates static functions, which was originally linked from here.

Delegation with Object.create()

// Renamed from ClassName since it's no longer a class per-say
// To emulate constructors, you can include an init function, however this is not part of delegation
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');
  }
  //, __proto__: null
};

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

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

// Object creation and delegation
var obj = Object.create(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'


// Object.create() under the hood works like:
Object.create = function (proto) { // Note: There's a property argument 
  var obj = {};
  obj.__proto__ = proto;
  return obj;
}

This is behaviour is exactly how native Javascript primitive objects handle inheritance. From this implementation, you should see why when you replace a method on a primitive object (eg. changing Array.prototype.length = function() { return 0; }) it would affect all functions that rely on it after.

However from what I've read on what delegation is by definition, one should be able to freely compose the call stack for delegation so you could specify if you wanted to delegate to for example prototypal A and C for one object but prototypal A and B for another object and be forced to have A always delegate to B to C. Thus below I've added an extra function to achieve this greater flexibility.

function objectDelegatation(obj) {
  // Skip over obj, getting the other parameters
  var prototypes = Array.prototype.slice.apply(arguments, 1);
  for (var i = 1; i < prototypes.length; ++i) {
    // Make the object that holds the methods 
    var intermediate = Object.assign({}, prototype[i]);
    //prototype.__proto__ = null; // The delegate prototypals would not have __proto__ set as Prototypal1 was
    
    // Make the __proto__ call stack
    prototype[i - 1].__proto__ = intermediate;
  }

  return obj;
}
var obj = {};
objectDelegatation(obj, Prototypal1, Prototypal2)

I give an implementation of Object.assign() in the next part on concatenation, or you can look at the MDN entry on it. This increased flexibility comes at the cost of slightly more memory cost, at least in this Javascript implementation since we want to leverage the __proto__ call stack, (you have to create a new object for the methods per object created) and actually puts it worse off than concatenation. However you can still access all delegate methods by climbing the prototype tree.

Some Notes

With the constructor pattern, object creation is built into the class/prototypal, whereas in the delegation pattern object creation is separate step from declaration of the prototypals (properly called prototypes, though it might be easier to think of them as delegate 'classes' but they are not classes). In other words, prototypes have no constructors. Additionally, the class pattern requires you to attach method definitions onto a ClassName.prototype as properties, which is a bit unintuitive and isn't quite the structure of seeing all the methods within a class definition that you typically expect. Thus the Object.create() pattern is more natural to prototypal inheritance.

.Prototype and .__proto__?
So the entire purpose of ClassName.prototype is to facilitate the use of the new keyword, otherwise it's a property like any other property on objects; __proto__ is actually where all the heavy lifting takes place. Keep in mind that generally, we should not be trying to manipulate __proto__ or do so with a command .setPrototypeOf() post object creation since Javascript compilers are not designed to optimise around that, as noted in the MDN entry here. Something to be noted is that function objects come with a .constructor property and if you want to mess with __proto__ directly for class implementations, you have to worry about. I'm not entirely sure how specifics of the how the command instanceof interacts with all these parts.

Another source of confusion is that some, for example Douglas Crockford, refer to prototypal inheritance in Javascript specifically to this delegation style of inheritance. This is of course because it makes use of the __proto__ call stack which is sort of synonymous with .prototype. However there is also concatenation style of prototypal inheritance.

For an alternative explanation, you find this youtube series by MPJ on object creation in Javascript useful. Also lookout for factory functions as the prototypal complement for constructors in part 4 of this series.

No comments:

Post a Comment

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