ES6 Class Multiple inheritance
I've done most of my research on this on BabelJS and on MDN (which has no information at all), but please feel free to tell me if I have not been careful enough in looking around for more information about the ES6 Spec.
I'm wondering whether or not ES6 supports multiple inheritance in the same fashion as other duck-typed languages do. For instance, can I do something like:
class Example extends ClassOne, ClassTwo {
constructor() {
}
}
to extend multiple classes on to the new class? If so, will the interpreter prefer methods/properties from ClassTwo over ClassOne?
Check my example below, super
method working as expected. Using a few tricks even instanceof
works (most of the time):
// base class
class A {
foo() {
console.log(`from A -> inside instance of A: ${this instanceof A}`);
}
}
// B mixin, will need a wrapper over it to be used
const B = (B) => class extends B {
foo() {
if (super.foo) super.foo(); // mixins don't know who is super, guard against not having the method
console.log(`from B -> inside instance of B: ${this instanceof B}`);
}
};
// C mixin, will need a wrapper over it to be used
const C = (C) => class extends C {
foo() {
if (super.foo) super.foo(); // mixins don't know who is super, guard against not having the method
console.log(`from C -> inside instance of C: ${this instanceof C}`);
}
};
// D class, extends A, B and C, preserving composition and super method
class D extends C(B(A)) {
foo() {
super.foo();
console.log(`from D -> inside instance of D: ${this instanceof D}`);
}
}
// E class, extends A and C
class E extends C(A) {
foo() {
super.foo();
console.log(`from E -> inside instance of E: ${this instanceof E}`);
}
}
// F class, extends B only
class F extends B(Object) {
foo() {
super.foo();
console.log(`from F -> inside instance of F: ${this instanceof F}`);
}
}
// G class, C wrap to be used with new decorator, pretty format
class G extends C(Object) {}
const inst1 = new D(),
inst2 = new E(),
inst3 = new F(),
inst4 = new G(),
inst5 = new (B(Object)); // instance only B, ugly format
console.log(`Test D: extends A, B, C -> outside instance of D: ${inst1 instanceof D}`);
inst1.foo();
console.log('-');
console.log(`Test E: extends A, C -> outside instance of E: ${inst2 instanceof E}`);
inst2.foo();
console.log('-');
console.log(`Test F: extends B -> outside instance of F: ${inst3 instanceof F}`);
inst3.foo();
console.log('-');
console.log(`Test G: wraper to use C alone with "new" decorator, pretty format -> outside instance of G: ${inst4 instanceof G}`);
inst4.foo();
console.log('-');
console.log(`Test B alone, ugly format "new (B(Object))" -> outside instance of B: ${inst5 instanceof B}, this one fails`);
inst5.foo();
Will print out
Test D: extends A, B, C -> outside instance of D: true from A -> inside instance of A: true from B -> inside instance of B: true from C -> inside instance of C: true from D -> inside instance of D: true - Test E: extends A, C -> outside instance of E: true from A -> inside instance of A: true from C -> inside instance of C: true from E -> inside instance of E: true - Test F: extends B -> outside instance of F: true from B -> inside instance of B: true from F -> inside instance of F: true - Test G: wraper to use C alone with "new" decorator, pretty format -> outside instance of G: true from C -> inside instance of C: true - Test B alone, ugly format "new (B(Object))" -> outside instance of B: false, this one fails from B -> inside instance of B: true
Link to fiddle around
An object can only have one prototype. Inheriting from two classes can be done by creating a parent object as a combination of two parent prototypes.
The syntax for subclassing makes it possible to do that in the declaration, since the right-hand side of the extends
clause can be any expression. Thus, you can write a function that combines prototypes according to whatever criteria you like, and call that function in the class declaration.
Sergio Carneiro's and Jon's implementation requires you to define an initializer function for all but one class. Here is a modified version of the aggregation function, which makes use of default parameters in the constructors instead. Included are also some comments by me.
var aggregation = (baseClass, ...mixins) => {
class base extends baseClass {
constructor (...args) {
super(...args);
mixins.forEach((mixin) => {
copyProps(this,(new mixin));
});
}
}
let copyProps = (target, source) => { // this function copies all properties and symbols, filtering out some special ones
Object.getOwnPropertyNames(source)
.concat(Object.getOwnPropertySymbols(source))
.forEach((prop) => {
if (!prop.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/))
Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
})
}
mixins.forEach((mixin) => { // outside contructor() to allow aggregation(A,B,C).staticFunction() to be called etc.
copyProps(base.prototype, mixin.prototype);
copyProps(base, mixin);
});
return base;
}
Here is a little demo:
class Person{
constructor(n){
this.name=n;
}
}
class Male{
constructor(s='male'){
this.sex=s;
}
}
class Child{
constructor(a=12){
this.age=a;
}
tellAge(){console.log(this.name+' is '+this.age+' years old.');}
}
class Boy extends aggregation(Person,Male,Child){}
var m = new Boy('Mike');
m.tellAge(); // Mike is 12 years old.
This aggregation function will prefer properties and methods of a class that appear later in the class list.
Justin Fagnani describes a very clean (imho) way to compose multiple classes into one using the fact that in ES2015, classes can be created with class expressions.
Expressions vs declarations
Basically, just like you can create a function with an expression:
function myFunction() {} // function declaration
var myFunction = function(){} // function expression
you can do the same with classes:
class MyClass {} // class declaration
var MyClass = class {} // class expression
The expression is evaluated at runtime, when the code executes, whereas a declaration is executed beforehand.
Using class expressions to create mixins
You can use this to create a function that dynamically creates a class only when the function is called:
function createClassExtending(superclass) {
return class AwesomeClass extends superclass {
// you class body here as usual
}
}
The cool thing about it is that you can define the whole class beforehand and only decide on which class it should extend by the time you call the function:
class A {}
class B {}
var ExtendingA = createClassExtending(A)
var ExtendingB = createClassExtending(B)
If you want to mix multiple classes together, because ES6 classes only support single inheritance, you need to create a chain of classes that contains all the classes you want to mix together. So let's say you want to create a class C that extends both A and B, you could do this:
class A {}
class B extends A {}
class C extends B {} // C extends both A and B
The problem with this is that it's very static. If you later decide you want to make a class D that extends B but not A, you have a problem.
But with some smart trickery using the fact that classes can be expressions, you can solve this by creating A and B not directly as classes, but as class factories (using arrow functions for brevity):
class Base {} // some base class to keep the arrow functions simple
var A = (superclass) => class A extends superclass
var B = (superclass) => class B extends superclass
var C = B(A(Base))
var D = B(Base)
Notice how we only decide at the last moment which classes to include in the hierarchy.
I made a library based on these principles that you can have a look at: mics
My answer seems like less code and it works for me:
class Nose {
constructor() {
this.booger = 'ready';
}
pick() {
console.log('pick your nose')
}
}
class Ear {
constructor() {
this.wax = 'ready';
}
dig() {
console.log('dig in your ear')
}
}
class Gross extends Classes([Nose,Ear]) {
constructor() {
super();
this.gross = true;
}
}
function Classes(bases) {
class Bases {
constructor() {
bases.forEach(base => Object.assign(this, new base()));
}
}
bases.forEach(base => {
Object.getOwnPropertyNames(base.prototype)
.filter(prop => prop != 'constructor')
.forEach(prop => Bases.prototype[prop] = base.prototype[prop])
})
return Bases;
}
// test it
var grossMan = new Gross();
grossMan.pick(); // eww
grossMan.dig(); // yuck!