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 1:

Subclass Factory Mixins

There is another way to realize mixins in Javascript: With subclass factories.

A subclass factory is a function that excepts a base class and returns an extended subclass of this base class:

const mixin = base => class extends base {
  /* properties to mix in */
}

Subclass factories are possible for two reasons:

  • classes can be defined as expressions and are first class* in Javascript
  • extends clauses can contain arbitrary expressions

Let's apply this pattern:

// superclass
class CartoonCharacter {
  constructor(author) { this.author = author }
  drawnBy() { return "drawn by " + this.author }
}

// mixin 1
const Human = base => class extends base {
  haveFun() { return "drinking beer" }
}

// mixin 2
const Simpson = base => class extends base {}

// composed subclass
const Homer = Simpson(Human(CartoonCharacter));

// create an instance
const homer = new Homer("Matt Groening")

console.log(homer.drawnBy());
console.log(homer.haveFun());

What are the advantages of mixins by sublcass factories?

  • natural property precedence: Later mixed in properties override prior properties of the same name
  • both mixins (Human/Simpson) and subclasses (Homer) can use super as usual
  • mixins can have constructors (stateful mixins)
  • no mutation of prototypes or instances

Stateful Mixins

State makes things hard. You should avoid state, whenever possible. However, sometimes your mixin needs its own state, which is passed through a constructor:

class CartoonCharacter {
  constructor(author) { this.author = author }
  drawnBy() { return "drawn by " + this.author }
}

const Human = base => class extends base {
  haveFun() { return "drinking beer" }
}

const Simpson = base => class extends base {
  constructor(arg, ...superArgs) {
    super(...superArgs);
    this.name = arg;
  }
  sayHey() { return "Hey, I am " + this.name }
}

const Homer = Simpson(Human(CartoonCharacter));
const homer = new Homer("Homer Simpson", "Matt Groening")

console.log(homer.drawnBy());
console.log(homer.haveFun());
console.log(homer.sayHey());

*the term first class functions means that functions can be passed around as arguments or return values like normal data

Solution 2:

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.