Parasitic Inheritance in Javascript using pure Pro

2019-02-14 19:42发布

问题:

I am learning OOP in JavaScript and have gone through various posts on the same. It has come to my knowledge that Douglas Crockford prescribes a pure prototypal approach to inheritance as opposed to the classical approach.

The following code taken from here implements Crockford's method:

var superInstance = {
  member1: 'superMember1',
  member2: 'superMember2'
};

var subInstance = Object.create(superInstance);
subInstance.member3 = 'subMember3';

I understand that Crockford's approach does away with constructor functions (correct me if I am wrong). Does this mean that the only way to initialize object members with this approach is by using object literals as shown in above code? Also, how do I implement parasitic inheritance with this approach to allow shared members, private variables and non-scalar values in the parent class (refer this post)?

Crockford mentions "maker functions" in his article, but does not give any example code for it. It would be great if someone could demonstrate parasitic inheritance using Crockford's pure prototypal approach.

Thanks.

回答1:

Misunderstanding about prototypal inheritance arise from the problem that in contrary to classical inheritance base constructor is not called to instantiate base object. Setting prototype to base object is not equivalent to classical inheritance, because prototype is shared among instances. As described in detail by Jimmy Breck-McKye. So to achieve parasitic inheritance you will have to follow two rules.

  1. Never define field members in prototype directly.
  2. Always call base constructor when instantiating successor

The latter can be achieved to one's taste using Object.create or assigning instance of base object directly to prototype. Given that Base is a constructor function, code for inheritance will be like below
Way #1

 function Base(){
 //a new object is created which is assigned to 'this'
 //this object has __proto__ === Base.prototype
     this.baseMember = 'I am the parent';
 }
 Base.prototype.baseMethod = function(){
     console.log('I am Base');
 };

 function Successor(){
 //a new object is created which is assigned to 'this'
 //this object has __proto__ === Successor.prototype

 //we override the object's property which is used for prototypal lookup
 //we lose members defined in Successor.prototype
      this.__proto__ = new Base();
 //we add a new property in the inherited object
      this.successorMember = 'I am a child';
 }
 Successor.prototype.successorMethod = function(){
     console.log('I am Successor');
 };

We will use defined constructor in the following manner

var child = new Successor();
//resulting in structure
//child: { //instance of Successor
//  successorMember: 'I am a child', 
//  __proto__: {//instance of Base
//     baseMember: 'I am the parent'
//     __proto__: {//Base.prototype
//        baseMethod : function 
//  }}}
console.log(child.successorMember);//accessible via direct property
console.log(child.baseMember);//accessible via prototype lookup
console.log('baseMethod' in child);//true, accessible via prototype lookup
console.log('successorMethod' in child);//false, method doesn't exist anywhere in the chain

Pay attention to the missing successorMethod defined via Successor.prototype. This happened because we've overridden __proto__ property of the object.

Way #2
Another way to override the __proto__ property is to call Object.create. However this function returns a new object and thus we will have to override the object that is returned by Successor constructor

 function Successor(){
 //a new object #1 is created which is assigned to 'this'
 //this object has __proto__ === Successor.prototype

 //a new instance #2 of Base is created with __proto__ === Base.prototype
 //a new object #3 is created with a __proto__ set to #2
      var successor = Object.create(new Base());
 //a new property is added to #1
      this.neverShowMember = 'I will not exist in resulting object';
 //a new property is added to #3 
      successor.successorMember = 'I am a child';
 //return of a non-primitive type object from constructor overrides the result
      return successor;//return object #3
 }

Let's examine usage of this approach in detail:

var child = new Successor();
//child: { //instance of Object
//  successorMember: 'I am a child', 
//  __proto__: {//instance of Base
//     baseMember: 'I am the parent'
//     __proto__: {//Base.prototype
//        baseMethod : function 
//  }}}
console.log(child.successorMember);//accessible via direct property
console.log(child.baseMember);//accessible via prototype lookup
console.log('baseMethod' in child);//true, accessible via prototype lookup
console.log('successorMethod' in child);//false, method doesn't exist anywhere in the chain

Resulting behaviour is pretty much the same. Pay attention to the missing neverShowMember although it was defined for this within constructor. It may be source of mistakes.

Way #3
Yet another way to inherit is not to mess around with proto chains. This approach is described in Jimmy Breck-McKye's article. I will skip detailed comments which were provided previously and will focus on changes

 function Successor(){
 //a new instance  Base is created with __proto__ === Base.prototype
      var successor = new Base();
 //extend object with a new property 
      successor.successorMember = 'I am a child';
      return successor;//return instance of Base with extra properties
 }

 var child = new Successor();
//child: { //instance of Base
//  successorMember: 'I am a child', 
//  baseMember: 'I am the parent'
//  __proto__: {//Base.prototype
//        baseMethod : function 
//  }} 
console.log(child.successorMember);//accessible via direct property
console.log(child.baseMember);//accessible via direct property
console.log('baseMethod' in child);//true, accessible via prototype lookup
console.log('successorMethod' in child);//false, method doesn't exist anywhere in the chain

You see that schema became flat. As an obvious conclusion you can't access base members if you override them. So if inside Successor we define

successor.baseMember = 'I am already grown enough!';

The instance (child) will lose access to baseIntance.baseMember which equals "I am the parent". In contrary to previous approaches when it would be accessible via child.__proto__.baseMember. But I believe this is not a common scenario when developing in javascript and should covered under another question.

Note that in all cases members defined in Successor.prototype are lost. You should take care of copying them manually in Successor constructor.

Conclusion
I hope this description was clear enough to understand that CraCrockford's object function

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

always requires a new instance passed as parameter o to achieve parasitic inheritance. Thus it's usage should look like below

var child = object(new Base());
child.successorMember = 'I am a child';

Same applies to the code from OP. To follow parasitic inheritance superInstance should be a new instance each time it is passed to Object.create. Thus it should be a factory function

var superInstance = function(){
    return {
      member1: 'superMember1',
      member2: 'superMember2'
    }
};

var subInstance = Object.create(superInstance());

or a constructor

function superInstance(){
    this.member1: 'superMember1',
    this.member2: 'superMember2'
};

var subInstance = Object.create(new superInstance());

Hope this helps