javascriptinheritancetraitsmixinsobject-composition

What are the practical differences between Mixins and Inheritance in Javascript?


Is just (simulating) multiple inheritance the only advantage of mixing:

 Object.assign( MyClassA.prototype, MyMixinB )

versus inheritance

 class MyClass extends MyClassB {

 // MyClassB = class version of MyMixinB

in ES6 Javascript?

Thanks


Solution

  • There will be not that one short answer. The practicality comes with the many different technical approaches (and how one combines them) that JavaScript (JS) does offer for building type/object systems.

    First, as the OP (original poster) already is aware of, there is no real/true multiple inheritance in JS. This language just offers prototype based single inheritance (that can be chained) regardless if one chooses classes or the traditional pure function based style.

    But, secondly, as in other programming languages (PL) too, there is another not inheritance based way of code reuse and object composition ... it's mixin and/or trait based composition.

    But I will stick to both of the by the OP provided example codes.

    Object.assign(MyClassA.prototype, MyMixinB)
    

    This already is a nice one, if one thinks of MyMixinB providing additional behavior as object based mixin like e.g. ...

    var arr = ['object', 'based', 'mixin', 'approach'];
    var list = { length: 4, 0: 'the', 1: 'quick', 2: 'brown', 3: 'fox' };
    
    console.log('(typeof arr.last) : ', (typeof arr.last));
    console.log('(typeof list.last) : ', (typeof list.last));
    
    const withObjectBasedListLastBehavior = {
      last: function () {
        return this[this.length - 1];
      }
    }
    Object.assign(Array.prototype, withObjectBasedListLastBehavior);
    Object.assign(list, withObjectBasedListLastBehavior);
    
    console.log('(typeof arr.last) : ', (typeof arr.last));
    console.log('arr.last() : ', arr.last());
    
    console.log('(typeof list.last) : ', (typeof list.last));
    console.log('list.last() : ', list.last());
    
    console.log('(arr.last === list.last) : ', (arr.last === list.last));
    console.log('(arr.last === Array.prototype.last) : ', (arr.last === Array.prototype.last));
    .as-console-wrapper { max-height: 100%!important; top: 0; }

    The above approach is a combination of composition and inheritance. The withObjectBasedListLastBehavior mixin provides a generically implemented last method for list like structures. This structure by itself does not effect anything. Via Object.assign the last list-behavior can be assigned to any list-like structure. But the OP with the first example does assign a mixin to a prototype. Thus there is no usage of such newly gained behavior unless an instance that does not feature an own last method is going to invoke it. Then prototypal delegation takes place ... the now prototypal last will be invoked within the context of the calling object.

    The advantage of providing additional behavior/functionality via mixins (structures that carry such behavior) comes with the flexibility of how this structures can be repeatedly used. The ideal mixin is atomic and features just one specific behavior (one method). This behavior than can be mixed from within the bodies of class constructors / constructor functions or, as with the above example, it gets assigned to any object (prototype objects or any other type). Thus code reuse in JavaScript via mixins/traits is possible at 2 levels ... at class level (at construction/instantiation time) and at any time at object level.

    This flexibility should be not that surprising since mixins/traits contribute to composition just the behavior point of view (an object has a behavior) whereas real inheritance, also in JS, is responsible for the is a thing relationship.

    One should be aware that the differences between mixin/trait based composition and inheritance is not a JavaScript specific thing. The concepts apply to other PLs too. Technically JS is just more flexible, for it is object based and does feature two ways of delegation, implicit via the automatically running prototypal delegation and explicit with passing context directly via invoking the call and apply methods of functions.

    A practical proof of the above provided strong opinion takes into account the OP's second example code ...

    class MyClass extends MyClassB {
    
    // MyClassB = class version of MyMixinB
    

    ... and does change it to something like that ...

    const withFunctionBasedListLastBehavior = (function () {
      function last() {   // single implementation
        return this[this.length - 1];
      }
      return function() { // always going to share the above
        this.last = last; // single implementation, thus always
      };                  // featuring one and the same signature.
    }());
    
    const withFunctionBasedListFirstBehavior = (function () {
      function first() {
        return this[0];
      }
      return function() {
        this.first = first;
      };
    }());
    
    const withFunctionBasedListItemBehavior = (function () {
      function item(idx) {
        return this[idx];
      }
      return function() {
        this.item = item;
      };
    }());
    
    
    class ListWrapper {
      constructor(list) {
    
        // mixin in / explicit delegation
        withFunctionBasedListFirstBehavior.call(list);
        withFunctionBasedListLastBehavior.call(list);
        withFunctionBasedListItemBehavior.call(list);
    
        // forwarding
        this.first = function() {
          return list.first();
        };
        this.last = function() {
          return list.last();
        };
        this.item = function(idx) {
          return list.item(idx);
        }
      }
    }
    
    
    class Queue extends ListWrapper { // inheritance part I.
      constructor() {
        const list = [];
    
        super(list);                  // inheritance part II.
    
        // queue specific behavior
        this.enqueue = function(value) {
          list.push(value);
          return value;
        };
        this.dequeue = function() {
          return list.shift();
        };
      }
    }
    
    
    var queue = new Queue;
    
    console.log("queue.enqueue('the') : ", queue.enqueue('the'));
    console.log("queue.enqueue('quick') : ", queue.enqueue('quick'));
    console.log("queue.enqueue('brown') : ", queue.enqueue('brown'));
    console.log("queue.enqueue('fox') : ", queue.enqueue('fox'));
    
    console.log("queue.first() : ", queue.first());
    console.log("queue.last() : ", queue.last());
    console.log("queue.item(2) : ", queue.item(2));
    
    console.log("queue.dequeue() : ", queue.dequeue());
    console.log("queue.dequeue() : ", queue.dequeue());
    console.log("queue.dequeue() : ", queue.dequeue());
    
    console.log("queue.first() : ", queue.first());
    console.log("queue.last() : ", queue.last());
    console.log("queue.item(2) : ", queue.item(2));
    
    console.log("queue.dequeue() : ", queue.dequeue());
    console.log("queue.dequeue() : ", queue.dequeue());
    
    console.log('(queue instanceof Queue) : ', (queue instanceof Queue));
    console.log('(queue instanceof ListWrapper) : ', (queue instanceof ListWrapper));
    .as-console-wrapper { max-height: 100%!important; top: 0; }