Skip to content

Instantly share code, notes, and snippets.

@maxnordlund
Last active December 18, 2016 11:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maxnordlund/ccb55cc7e58567f78a0986885bfbf755 to your computer and use it in GitHub Desktop.
Save maxnordlund/ccb55cc7e58567f78a0986885bfbf755 to your computer and use it in GitHub Desktop.
Multiple inheritance in JavaScript
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)
}
}
@maxnordlund
Copy link
Author

maxnordlund commented Dec 17, 2016

You use it like so:

import mixin from "./multiple-inheritance.js"

class Foo extends mixin(Bar, Baz) {
  constructor() {
     super() // Call each super class in the same order as given above
  }

  method(parameter) {
    // No super call here due to implementation, but you can write this instead
    super.Bar.method.apply(this, arguments)

    super.Bar === Bar.prototype
    super.Baz === Baz.prototype
  }
}

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 the mixin 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment