Search code examples
javascriptinheritanceprototypal-inheritance

Multiple prototypical inheritance in Javascript


I have 2 base classes, say ParentClass1 and ParentClass2. Now I want to do multiple prototypical inheritance to ChildClass.

With a single parent class, I had my code as follows.

var ParentClass = function() {

};

ParentClass.prototype.greetUser = function(name) {
    console.log('Hi. Hello,', name);
};

var ChildClass = function(name) {
  this.greetUser(name);
};

ChildClass.prototype = Object.create(ParentClass.prototype);

var obj = new ChildClass('John');
// Hi. Hello, John


And now when I have to inherit from 2 parent classes, I tried the following code.

var ParentClass1 = function() {

};

ParentClass1.prototype.greetUser = function(name) {
    console.log('Hi. Hello,', name);
};

var ParentClass2 = function() {

};

ParentClass2.prototype.askUser = function(name) {
    console.log('Hey, how are you,', name);
};

var ChildClass = function(name) {
  this.askUser(name);
  this.greetUser(name);
};

ChildClass.prototype = Object.create(ParentClass1.prototype);
ChildClass.prototype = Object.create(ParentClass2.prototype);

var obj = new ChildClass('John');
// Error.

But this seems like, this will only accept the last mentioned Object.create().

So later, It tried switching the second Object.create() to Object.assign(), and then it worked fine.

ChildClass.prototype = Object.create(ParentClass1.prototype);
ChildClass.prototype = Object.assign(ChildClass.prototype, ParentClass2.prototype);

But my concern is Object.assign() is doing a clone. So is that the right way to do it? Or are there any better alternatives?

Sorry for making this question lengthy. I really appreciate any help you can provide.

NOTE: I had a brief look into this and this


Solution

  • It is not very useful to do two assignments to the prototype property, as the last one will overwrite the first. You can do like this, since Object.assign accepts more arguments:

    Object.assign(ChildClass.prototype, ParentClass1.prototype, ParentClass2.prototype);
    

    Note that Object.assign performs a shallow copy. That a copy has to be made is sure: you need a prototype object that is different from both other prototypes: the union of both. So inevitably you need to somehow copy the members of the parent prototypes into your target prototype object.

    Some caveats:

    1. Object.assign makes a shallow copy

    Since Object.assign performs a shallow copy, you might get into cases where you interfere with the parent prototype. This might be what you want or do not want.

    Example:

    var ParentClass1 = function() {
    
    };
    
    ParentClass1.prototype.userList = [];
    ParentClass1.prototype.addUser = function(name) {
        this.userList.push(name);
    };
    
    var ParentClass2 = function() {
    
    };
    
    ParentClass2.prototype.askUser = function(name) {
        console.log('Hey, how are you,', name);
    };
    
    var ChildClass = function(name) {
      this.askUser(name);
    };
    
    Object.assign(ChildClass.prototype, ParentClass1.prototype, ParentClass2.prototype);
    
    var p = new ParentClass1('Parent');
    var obj = new ChildClass('John');
    obj.addUser('Tim'); // Added to child, but
    console.log(p.userList); // now parent also has Tim...

    2. Object.assign only copies enumerable properties

    This means that in some cases you will not get the properties you had hoped for. Say you wanted to inherit also from Array.prototype, then you would want your child object to have a length property, but since it is not enumerable, you will not get it with Object.assign:

    var ParentClass2 = function() {
    
    };
    
    ParentClass2.prototype.askUser = function(name) {
        console.log('Hey, how are you,', name);
    };
    
    var ChildClass = function(name) {
      this.askUser(name);
    };
    
    Object.assign(ChildClass.prototype, Array.prototype, ParentClass2.prototype);
    
    var obj = new ChildClass('John');
    console.log(obj.length); // undefined
    console.log(Array.prototype.length); // 0

    3. Object.assign executes getters

    Object.assign cannot copy getters. Instead it executes them to retrieve the value for the copy. Executing code on the parent prototype may have effects (by design of that getter) on the state of the parent prototype. This might be undesired behaviour in the context of this copy.

    Secondly, the value of the getter can be the result of some calculation and state of the object, returning different values each time it is referenced. But object.assign will only reference it once, and then create a property that always has that single value. See the effect in this example:

    var ParentClass1 = function() {
    
    };
    
    // Define a getter on the prototype which returns a
    // random number between 0 and 999, every time it is referenced:
    Object.defineProperty(ParentClass1.prototype, 'randomNumber', { 
        get: function() {
            return Math.round(Math.random() * 1000); 
        }, 
        enumerable: true 
    });
    
    var ParentClass2 = function() {};
    
    ParentClass2.prototype.askUser = function(name) {
        console.log('Hey, how are you,', name);
    };
    
    var ChildClass = function(name) {
      this.askUser(name);
    };
    
    Object.assign(ChildClass.prototype, ParentClass1.prototype, ParentClass2.prototype);
    
    var p = new ParentClass1('Parent');
    var obj = new ChildClass('John');
    console.log('different:');
    console.log(p.randomNumber);
    console.log(p.randomNumber);
    console.log(p.randomNumber);
    console.log('always same:');
    console.log(obj.randomNumber);
    console.log(obj.randomNumber);
    console.log(obj.randomNumber);
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    Further reading

    The concept of combining multiple prototypes into a new one is often coined "mixin". Here are some related Q&A: