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:
-
class
es 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 usesuper
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:
-
Object.assign
only copies enumerable properties of an object. However, the methods and properties of a class are non-enumerable. - 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.