mics
mics copied to clipboard
Interesting!
I was making a tool called multiple, used like this:
class Foo { ... }
class Bar extends Foo { ... }
class Baz extends SomethingElse { ... }
// the magic:
class Lorem extends multiple(Bar, Baz) { ... }
const l = new Lorem
l instanceof Lorem // true
l instanceof Foo // true
l instanceof Bar // true
l instanceof Baz // true
l instanceof SomethingElse // true
(requires Symbol.hasInstance tricks)
At the time, Proxy was not yet supported in browsers, so here's my first two implementations not using Proxy. They work in many cases, but they fail in some cases:
- https://gist.github.com/trusktr/05b9c763ac70d7086fe3a08c2c4fb4bf
- https://gist.github.com/trusktr/8c515f7bd7436e09a4baa7a63cd7cc37
((mostly?) working examples at the bottom of the files)
However, now that Proxy is becoming a real thing (and polyfills have gotten better, though not perfect), I'd like to give a third implementation a go. Using Proxy, it is theoretically easy to make this work (well, maybe now that Proxy is out, it's not Theoretical anymore!).
I really like this concept, because it means that for the end user, they don't have to worry about using any APIs when they define classes.
For example, a user can simply define a class like this:
export default
class Bar extends Foo {
// ...
}
and they don't have to do anything special for it to be used in multiple inheritance by someone else.
Then, someone else can use multiple if they want multiple inheritance:
import Bar from './path/to/Bar'
import Baz from './path/to/Baz'
import { multiple } from './path/to/utilities'
class Lorem extends multiple(Bar, Baz) { // order matters
// ...
}
However, as you can imagine, mixing classes that have different constructor signatures will become a pain.
So I was also experimenting with a way to make it possible to call any of the constructors (using the new Reflect.construct) in any order, with specific args:
class Lorem extends multiple(Bar, Baz) { // order matters
constructor() {
// order would matter, the user of `multiple` has to know
// what are the consequences of calling constructors in different orders:
this.callSuperConstructor(Bar, arg1, arg2, ...)
this.callSuperConstructor(Baz, arg1, ...)
}
}
your point?
@minecrawler My point is only to share ideas. See @Download's comment here.
@Download I noticed you mentioned that your API returns an ES5-style constructor. What happens when the user defines classes using super? Have you had problems with that? For example, this and this (excuse my alarming titles, just that I was expecting new JS features to be dynamic like pre-ES6 JavaScript).
@minecrawler The point is I think to share ideas and hopefully eventually build the ultimate multiple inheritance library for JS (dream big!) :)
@trusktr I can definitely see the advantage of being able to mix multiple regular classes. But indeed super calls will become very difficult probably.
About the ES5 constructor... It's actually more of a shorthand / alias. The actual instance returned is an instance of an ES6 class. I really like the ES6 classes spec, but I dislike the mandatory use of new. I understand it but I would have liked it more if they had taken measures that would just make it optional.
So the use of super should work as it also does for normal ES6 classes. mics basically creates a normal ES6 class with a normal inheritance chain. The only trick is that we postpone the decision on which classes are in the inheritance chain to the last possible moment.
But indeed super calls will become very difficult probably.
If an ES6 mixin class has a constructor, it has the same problem (with ES5 classes we can just .call constructors in any order with any args). As a convention, I make all my mixin classes take a single "options" argument (it can be called anything, as long as each class accepts a single object with args inside of the object).
f.e.
function FooMixin(base) {
return class Foo extends base {
constructor(options = {}) {
super(options)
if (options.something) { ... }
this.bar = options.bar
}
}
}
then this way the number and shape of the arguments can be anything and each constructor can read only what they care about:
new SomeClassWithFooMixedIntoIt({
thisMightBe: 'an option that Foo does not care about',
something: 'an option that Foo does care about',
bar: 'bar',
})
I understand it but I would have liked it more if they had taken measures that would just make it optional.
Yeah, i miss the ES5 flexibility with ES6 classes too.
then this way the number and shape of the arguments can be anything and each constructor can read only what they care about:
I really like this idea. Starting a new project this week and will give this a shot
One big drawback I see of using an object as your one-stop argument is typehinting in IDEs. Have to really keep up on my JSDoc's to get around that problem (not necessarily a bad thing)
I agree, being able to specify the actual args would be nicer. That's why I had tried to implement the callSuperConstructor helper in my multiple implementation. At some point I'll give version 3 a shot with real Proxies, and specific args rather than objects.