Last active
December 18, 2016 11:10
-
-
Save maxnordlund/ccb55cc7e58567f78a0986885bfbf755 to your computer and use it in GitHub Desktop.
Multiple inheritance in JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default function createMixinFrom(...classes) { | |
let proto = Object.create(null) | |
function AnonymousMixinClass(...parameters) { | |
let proto = Object.getPrototypeOf(this) | |
// Run all super class constructors in order they were given. | |
// *This doesn't work without babel, sadly, because classes can't be called | |
// with an user supplied context* | |
classes.forEach(klass) => { | |
// Set the prototype before each call to trick the constructor into | |
// submission, it's fine, really... | |
Object.setPrototypeOf(this, klass.prototype) | |
klass.apply(this, parameters) | |
}) | |
// ...but restore the prototype before we leave | |
Object.setPrototypeOf(this, proto) | |
} | |
// Install a Proxy as the prototype in ordr to trap interresting stuff, like | |
// arbitrary property access. But use an null object to avoid breaking | |
// invariants. | |
AnonymousMixinClass.prototype = new Proxy( | |
proto, new MulipleInheritanceProxy(classes) | |
) | |
// Install custom `instanceof` handler in all super classes, | |
// and on the anonymous class as a shorthand | |
classes.forEach((klass) => { | |
let zuper = klass[Symbol.hasInstance] || () => false | |
proto[klass.name] = klass.prototype | |
klass[Symbol.hasInstance] = function hasInstance(target) { | |
return target instanceof AnonymousMixinClass || zuper.call(this, target) | |
} | |
}) | |
return AnonymousMixinClass | |
} | |
class MulipleInheritanceProxy { | |
constructor(classes) { | |
super() | |
this.classes = classes | |
} | |
get(target, property, receiver) { | |
// Fulfill all invariants by checking the target first, this includes its | |
// normal prototype chain | |
if (property in target) return target[property] | |
let superClass = classes.find((klass) => property in klass.prototype) || {} | |
return Reflect.get(superClass, property, target) | |
} | |
set(target, property, value, receiver) { | |
// Fulfill all invariants by checking the target first, this includes its | |
// normal prototype chain | |
if (property in target) return target[property] = value | |
let superClass = classes.find((klass) => property in klass.prototype) || {} | |
return Reflect.set(superClass, property, value, target) | |
} | |
has(target, property) { | |
// Fulfill all invariants by checking the target first, this includes its | |
// normal prototype chain | |
return property in target || classes.some((klass) => property in klass.prototype) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You use it like so:
Currently only works with babel due to class constructors not allowed to be called using
.call/.apply
. The diamond problem handled by checking each super class, and their ancestors, in the same order as given to themixin
decorator.Due to implementation constraints, super calls only work in the constructor, where it's required. See above for workaround, and the
super.<super class>
shorthand. While this is kinda wonky, it does make it crystal clear which super method you are actually calling.In order to support something like that, I need to return a stub method on every method access (properties that happen to be functions). Then call each super method in the correct order. I could avoid using the proxy by taking the union all the super classes method sets together and creating a stub method for each, but if you mutate any of the super classes then I'll still need the proxy trap. In the end, I think it's clearer to just type out the super class you are interested in by hand.