Search code examples
javascriptecmascript-6traitsmixinscomposition

Mixins for ES6 classes, transpiled with babel


According to various sources (2ality, esdiscuss) one should be able to add mixins to classes:

EDIT discovered that class methods are not enumerable so that cannot work. Edited the code below, but still no joy

class CartoonCharacter {
  constructor(author) {
    this.author = author;
  }

  drawnBy() {
    console.log("drawn by", this.author);
  }
}

// THIS CANNOT WORK
// class methods are not enumerable
// class Human {
//  haveFun() {
//    console.log("drinking beer");
//  }
// }

let Human = Object.create({}, {
  haveFun:   {
    enumerable: true,
    value: function () {
      console.log("drinking beer");
    }
  }
});

class Simpson extends Object.assign(CartoonCharacter, Human) {
  constructor(author) {
    super(author);
  }
}


let homer = new Simpson("Matt Groening");
homer.drawnBy();  // expected: drawn by Matt Groening
homer.haveFun();  // expected: drinking beer

I get the "drawn by Matt Groening" but instead of the "drinking beer" I get an error

-> Uncaught TypeError: E.haveFun is not a function


Solution

  • There are two problems with your mixins:

    1. Object.assign only copies enumerable properties of an object. However, the methods and properties of a class are non-enumerable.
    2. The methods and properties of a class are not defined on the constructor. They are defined on the prototype of the constructor.

    This is how you would extend a class using mixins:

    class CartoonCharacter {
      constructor(author) {
        this.author = author;
      }
    
      drawnBy() {
        console.log("drawn by", this.author);
      }
    }
    
    class Human {
      haveFun() {
        console.log("drinking beer");
      }
    }
    
    mixin(CartoonCharacter, Human);
    
    class Simpson extends CartoonCharacter {
      constructor(author) {
        super(author);
      }
    }
    
    
    let homer = new Simpson("Matt Groening");
    homer.drawnBy();  // expected: drawn by Matt Groening
    homer.haveFun();  // expected: drinking beer
    
    function mixin(target, source) {
      target = target.prototype; source = source.prototype;
    
      Object.getOwnPropertyNames(source).forEach(function (name) {
        if (name !== "constructor") Object.defineProperty(target, name,
          Object.getOwnPropertyDescriptor(source, name));
      });
    }
    

    It works as expected in Babel: demo.