Accesssing ES6 class constructor function
I don't think it is possible to call the constructor as a function. However, in JavaScript a class is just an object (unlike C# or Java, where it's meta data), which means that you can pass it around and dynamically extend it. E.g. you can intercept class creation like this:
function enhanceClass(c) {
return class EnhancedClass extends c {
constructor() {
//do your magic here
}
}
}
class MyClass {}
const EnhancedMyClass = enhanceClass(MyClass);
new EnhancedMyClass();
As for toString
& not being able to call a function directly:
toString
is already overwriteable, and in ES5 code it wasn't uncommon
to throw from a constructor if it wasn't called as one (for good
reasons). ES6 classes just do that for you.
It means testing something for typeof "function" no longer guarantees it can be invoked without error.
This has never been a guarantee. Consider this code
function Test() {
throw new TypeError();
}
typeof Test === 'function'; // true
Test(); // TypeError
About the stringifications problems, I don't understand what you are attempting to do. Usually you shouldn't need to stringify a function. Probably there is a better approach.
There seems to be no method [...] to invoke a class constructor except by creating an instance of the class.
Well, the point of constructors is creating instances. If you don't want the instance you can just discard it, but then why call the constructor?
So classes are fundamentally different than prototype contructors in how we can use them, far more than syntactic sugar
Not that different. You can usually translate one syntax to the other, but you may need ES6 features like new.target
, super
or setPrototypeOf
.
@Oriol
This has never been a guarantee
I mean a guarantee that I can simply invoke a function. What the function actually does is of course beyond my control.
About the stringifications problems
I'm not really concerned about this, but rather just noting it out as something that is fundamentally different -- we now have something of type "function" where Function.protoype.toString() doesn't evaluate to legal javascript function declaration. But this point is a distraction from the real concern, yeah -- nothing was ever guaranteed ehre
@Tiddo
(slaps forehead) this might - actually - be enough. I need to think about it a bit. The issues of concern are: ability to name the resulting class dynamically (which I remember some quasi-hack involving using dynamic properties of an object that it's possible), and having a return value from "super" (is that possible? to be compatible with ES5 prototypes that return something from the constructor);
But this does kinda get to the major issue... I think my head is so far in how we do things with prototypes that it didn't occur to me to just use an ES6 language feature. I think the biggest difficulty would be creating an implementation that works with native ES6 constructs as well as ES5 or transpiled classes, but seems like it should be possible.
On Thu, Jan 5, 2017 at 5:31 PM, James Treworgy <jamietre at gmail.com> wrote:
I can't address your questions about "why" (I wasn't plugged into the discussions around it), but addressing this:
This has come into play lately for me, as an DI container we use that does exactly this doesn't work with ES6 classes (and as far as I can tell, there's no way to make it work, other than having devs no longer use class syntax).
Can you clarify what prevents it from being made to work? I'm probably missing the point you're making there. For instance, this does some brain-dead DI (injecting an argument in the constructor) by dynamically extending the class:
// The class we'll do DI on
class Original {
constructor($foo) {
this.foo = $foo;
}
run(num) {
const result = this.foo.fooMethod(num);
console.log(`num is ${num}, result is ${result}`);
}
}
// Brain-dead di function
const di = (cls, Foo) => {
const o = {
[cls.name]: class extends cls {
constructor(...args) {
super(new Foo(), ...args);
}
}
};
return o[cls.name];
};
// Ues a class that's been DI'd
const use = Original => {
new Original().run(42);
};
// Use it in dev
use(di(Original, class Foo {
fooMethod(num) {
return num * 2;
}
}));
// Use it in production
use(di(Original, class Foo {
fooMethod(num) {
return num / 2;
}
}));
That outputs
num is 42, result is 84
num is 42, result is 21
...because of the different injected Foo
s. (This is obviously a
simplistic example.)
Separately, there are some tools you can use, such as
Reflect.construct
, but granted that does create an instance. For
instance, if for some reason you wanted to extend a class without using
class
:
class A {
amethod() {
console.log("amethod");
}
}
function B() {
const t = Reflect.construct(A, [], B);
return t;
}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
B.prototype.bmethod = function() {
console.log("bmethod");
};
const b = new B();
b.amethod(); // "amethod"
b.bmethod(); // "bmethod"
console.log(b instanceof A); // true
console.log(b instanceof B); // true
Of course, that cheats a bit with that return t;
. :-)
There are probably some tools that should be added to the list. For
instance, there's this proposal for Reflect.isCallable
and
Reflect.isConstructor
). And my bmethod
above isn't really a method, so
it wouldn't be able to use super
; in theory one could argue for a
Reflect.makeMethod
(but use cases are limited, given class
syntax). New
tools can be added if persuasive use cases come up (and people step forward
to define them and get a champion on board).
But circling back, I could be well wide of the mark above. If you can give us more specifics about use cases that aren't supported, we can probably do better helping with them.
-- T.J.
Apologies, my email was apparently slow and I didn't see your reply to Oriol.
On Thu, Jan 5, 2017 at 6:28 PM, James Treworgy <jamietre at gmail.com> wrote:
(slaps forehead) this might - actually - be enough. I need to think about
it a bit. The issues of concern are: ability to name the resulting class dynamically (which I remember some quasi-hack involving using dynamic properties of an object that it's possible)
Yup. :-) Coincidentally, I demonstrated it in my previous note, see the
di
function where it uses cls.name
as a computed property.
, and having a return value from "super" (is that possible? to be compatible with ES5 prototypes that return something from the constructor);
...and my Reflect.construct
example may (or may not!) be relevant there.
Best,
-- T.J.
Can you clarify what prevents it from being made to work?
The fundamental feature difference (continuing to think about this!) is that with ES5 constructors, I can create an instance of something from an abitrary constructor provided to me, and inject properties that will be available to it at contruction time. The basic operation of the container might be like this
function createInstance(Cotr, args /* array */) {
function F() {
// this is dynamic in reality but simple example of injecting
something...
this.logger = new Logger();
var instance = Cotr.apply(this, args)
return instance; // in case Cotr returns something
}
F.prototype = Cotr.prototype;
return new F();
}
So the Cotr can refer to "this.logger" in the constructor. I don't think there's a way to do this with dynamic class inheritance since you always have to call super() before you can assign any properties in a constructor.
This isn't a dealkiller for the tool overall - I can constructor injection instead:
class Thing
static _inject = [Logger]
constructor(deps, ...args) {
// deps = { logger: Logger, dep2: Dep2, ... }
Object.assign(this, deps) // or however you want to make them
available
}
}
This will work fine with the dynamic subclass pattern. it just was nice to have everything done by the framework and get rid of boilerplate, but this also has benefits of classes working without the DI container. :) I'm bringing this into an ES6 project for the first time so I can live with a different pattern.
I too was surprised and disappointed with this restriction on constructors.
The enforcement seems pointless to me, because there are valid patterns such as DI or multiple-inheritance where the JS run-time cannot know how a framework is trying to accomplish the construction sequence.
This is really counter to the general philosophy of JS (imho) and more akin to static languages like Java or C#... over there meta programming (MP) happens in very different ways, but JS had previously made this task simple... sadly this kind of thing starts to make MP harder at every step.
I don't hold much hope that this will be relaxed but it should be. :(
Best, Don
Don Griffin Director of Engineering Sencha, Inc. www.sencha.com
The enforcement seems pointless to me
So we're all on the same page, this restriction exists because it means
class
syntax can extend builtin types like Map
, Set
, Array
and
Error
and such, which cannot be extended (with correct functionality
anyway) in standard ES5. By delaying initialization of this
, it allows
the constructor to drill all the way to the base class, which gets to
instantiate the correct type of object. To have this
be initialized up
front, there would need to be some other mechanism in place to instantiate
the correct type of base object.
I feel your pain though, I ran into similar issues when updating our codebase to ES6 classes, though I was able to work around it for our specific usecase by having a base class handle hooking into things.
On 1/5/17 2:21 PM, James Treworgy wrote:
function createInstance(Cotr, args /* array */) { function F() { // this is dynamic in reality but simple example of injecting something... this.logger = new Logger();
var instance = Cotr.apply(this, args)
So the thing is...
This works for script-defined constructors. But it never worked right for constructors of built-ins.
return instance; // in case Cotr returns something
At which point you've lost your logger, right?
Specifically, in the ES5 world, if the Cotr that's passed in there is Array or Date, say, then instance != this and you don't get the logger thing.
The behavior of ES6 constructors is meant to be able to explain the construction of such built-ins (and especially the new ones like Map, as well as the various DOM built-ins in browsers) and to allow subclassing such built-ins usefully.
So the Cotr can refer to "this.logger" in the constructor. I don't think there's a way to do this with dynamic class inheritance since you always have to call super() before you can assign any properties in a constructor.
That's unavoidable if you want to allow Cotr to control the actual object allocation, which was one of the design constraints here to enable built-ins to work the same way as non-built-ins.
So we're all on the same page, this restriction exists because it means
class
syntax can extend builtin types likeMap
,Set
, Arrayand
Error` and such, which cannot be extended (with correct functionality anyway) in standard ES5
That makes sense for built-ins, but is there some reason this enforcement cannot be applied for those constructors (perhaps by them) and not all constructors?
(btw - thanks for explaining the point of this - I was not aware of that)
Best, Don
Don Griffin Director of Engineering Sencha, Inc. www.sencha.com
At which point you've lost your logger, right?
That's up to the constructor that got passed in - the only job of "createInstance" is to make sure that the client constructor has the dependencies available to it in some way, ideally one that doesn't require writing boilerplate in every class. If a constructor chooses to return something other than it's own instance, I don't really care whether or not it exposes logger, because those dependencies are only for its own use anyway.
The actual pattern might not be to assign the deps to a public property
anyway, it could be assign them all to this[_someSymbol]
or whatever. The
only goal is to make them already available to it, in whatever convention
we choose in the app, without having to pollute the actual constructor with
a "deps" argument and boilerplate to assign them in every class.
So I get that this decision has to do with making builtins extensible, but I don't totally understand why it has to apply to everything, or even why we can't just allow people mis-using the builtin constructors to have strange behavior, as we do with Array in ES5.
That is, if I was simply allowed to access "Cotr" as a function, why would anything else cease to work? We remove some risk of abuse, but seems a bit like we threw the baby out with the bathwater. We could still have the class syntax work the same way, but not prevent people from accessing their own constructor functions.
I mean, nothing's stopping me writing classes like this:
class Thing { constructor(...args) { Thing.cotr.call(this, ...args) } }
Thing.cotr = function() { ... }
Now I have access to my constructor function. I could just write a babel plugin to convert all my classes to this syntax :) But it all seems so unnecessary
.. heh well I guess if I was going to write a babel plugin to deal with this I'd just have it not do the "new" check anyway :) this really only applies to the actual spec / native implementation.
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20170105/bcb2953c/attachment-0001
Thanks, that's pretty interesting. Curiously, my use case isn't part of the discussion - the proposals are focused on new syntax to deal with the "99.9%" case of wanting a factory. Not really addressed is simply "people may want to call it for some other reason", I guess I'm the 0.1% case.
It seem to me that allowing invocation through function.call
or
function.apply
would be pretty reasonable. Using these mechanisms is very
explicit. You would not lose the protection gained by prohibiting normal
new-less calls, and it wouldn't require creating any new syntax to expose
the function.
"YK: lets go back to es5, a massive hazard to forget use. This made many people not want the newless behavior."
This is fine (though not sure I agree it was such a massive hazard in practice - linters have done a fine job of letting us know when we screwed up for a long time) - but it seems that prohibiting invication through any and all means is overkill - and has negative consequences for advanced use cases and interop with ES5.
"MM: we should drop this completely, and solve it with decorators."
I'm not sure what that would look like but I think it only refers to newless construction..
Thanks for the link. It seems that discussion is regarding an aesthetic concern of "foo = Foo()" being allowed when today "foo = new Foo()" is required. I assume these are entangled concerns but the issue James mentioned with DI and I've hit with multiple-inheritance are the restriction on ".call()" and ".apply()" being used on constructor functions.
Is there a way to separate these? I see the use of "foo = Foo()" as a cosmetic (and therefore perhaps not worth a lot of effort) while the other holds more fundamental implications for DI and MI solutions.
Best, Don
Don Griffin Director of Engineering Sencha, Inc. www.sencha.com
On Thu, Jan 5, 2017 at 7:21 PM, James Treworgy <jamietre at gmail.com> wrote:
Can you clarify what prevents it from being made to work?
The fundamental feature difference (continuing to think about this!) is that with ES5 constructors, I can create an instance of something from an abitrary constructor provided to me, and inject properties that will be available to it at contruction time.
and
On Fri, Jan 6, 2017 at 3:49 AM, Don Griffin <don at sencha.com> wrote:
...the issue James mentioned with DI and I've hit with multiple-inheritance are the restriction on ".call()" and ".apply()" being used on constructor functions.
Thinking out loud:
Both James' DI case and the multiple-inheritance case could be
addressed with a Reflect utility function that allows you to provide a
hook triggered when this
is being allocated for the base
constructor. This is basically call
for constructors, but with a
hook rather than a thisArg. Just to have a name/concept for it:
Reflect.new(targetConstructor, hook, ...args)
. For now let's assume
hook
is a simple function that receives the freshly-allocated this
and can either augment it or return a replacement, but I'll circle
back to that later.
James' example:
let o = Reflect.new(Ctor, thisObj => {
thisObj.logger = new Logger();
});
Let's assume Ctor
extends Base
; it would work like this:
- Call EvaluateNew(
Ctor
,args
,hook
), note the new third argument. EvaluateNew passes the hook to Construct, which passes it to [[Construct]]. - Eventually during that [[Construct]],
Ctor
callssuper
, which callsBase
's [[Construct]], also passing in the hook. (I don't know yet howsuper(...)
has access to the hook; I guess we'd have to have it on the environment, like [[NewTarget]].) - We reach Step 5 of
Base
's [[Construct]] call. SinceBase
's kind is "base", we perform OrdinaryCreateFromConstructor, but then pass the result through the hook. Since this particular hook doesn't return an object, thisArgument is set to the result from OrdinaryCreateFromConstructor as usual. - Construction completes as normal.
So even the base constructor sees James' logger
property on this
by the time it has a this
, because the hook gets a chance to augment
it before the base constructor code runs.
An MI example with A
, B
, and C
, assuming they're all base constructors:
let o = Reflect.new(C, () =>
Reflect.new(B, () =>
new A()
)
);
// (Presumably do some mixing in of prototype properties...)
Execution is like before, but in this case when we reach Step 5 of
[[Construct]] for B
and C
, our hook returns an object, and so
thisArgument is set to that object rather than the one from
OrdinaryCreateFromConstructor.
I said I'd circle back on hook being a simple function: The MI example
above creates and throws away two objects it doesn't need to (the ones
created for B
and C
but then replaced by the hook). If that's a
concern, we can make hook an object with allocate
and/or
postAllocate
properties: allocate
would provide this
(MI),
postAllocate
would just augment it (James' DI example). Or whatever.
That's jumping forward to design; we're still at the concept stage and
may well never reach design.
If needed for things like Error
, constructors could have a flag
indicating that they cannot accept the hook (or at least, cannot
accept the hook providing a different this
), causing a throw at Step
5 of [[Construct]].
Conceptually simple. Not necessarily simple in terms of impacts on specification or implementations. In terms of the spec, we have at least:
- Adding
Reflect.new
(or whatever it's called) - Modifying EvaluateNew (here)
- Modifying Construct (here)
- Modifying [[Construct]] (here)
- Keeping track of the hook somewhere such that evaluating
super(...)
(here) can pass Construct the hook, possibly another slot on environment records - Possibly a flag slot or similar on functions like
Error
, if needed
I'm not competent to speak to impacts on implementations.
Which all sounds like a lot, but (modulo implementation complexity) I
don't think it really is, and I think it's basically what we'd need to
do to make call
/apply
work with constructors anyway (just passing
around a hook function/object rather than a thisArg), since we
wouldn't want call
/apply
to allow violating the "no this
before
super(...)
" rule by setting the this
binding early.
-- T.J.
T.J. Thanks for the very thoughtful analysis. But I keep coming back to this:
since we wouldn't want
call
/apply
to allow violating the "nothis
beforesuper(...)
" rule by setting thethis
binding early.
Why?
To me the best solution is also the simplest - just let people do this. Not only does it avoid all the implementation complexity, but it also provides feature parity between ES6 classes and conventional prototype constructor functions.
Having some mechanism (e.g. via Reflect) would be better than nothing, but it would also mean you have to write two versions of anything to support both ways of creating prototypes.
It seems like there should be a very compelling reason to prevent someone
from using a feature. The risk of accidental "new" doesn't really apply to
call
and apply
-- and even in the direct invocation situation I don't
think it's compelling either (since we've had that since day 1 and managed
to get by :) but I don't really care if that's prevented.
Note: Related discussion of calling class constructors here: esdiscuss.org/topic/determine-if-a-value-is-callable-constructible
On Fri, Jan 6, 2017 at 12:11 PM, James Treworgy <jamietre at gmail.com> wrote:
T.J. Thanks for the very thoughtful analysis. But I keep coming back to this:
since we wouldn't want
call
/apply
to allow violating the "nothis
beforesuper(...)
" rule by setting thethis
binding early.Why?
To me the best solution is also the simplest - just let people do this.
On first blush, a couple of reasons, but that doesn't mean they aren't addressable:
-
I think the supposed simplicity is illusory. If you get into the mechanics of actually "just" allowing people to do it with
call
/apply
, as I said earlier, I think it looks very likeReflect.new
but with less flexbility and with the "this
beforesuper()
" problem. Do you have a proposal for how to actually do it that doesn't have similar complexity? You'd have to modifycall
andapply
, you'd still have to modify how Construct and [[Construct]] work (at the very least). Maybe you get some savings by allowing athis
binding beforesuper
(you might avoid the hook slot on the environment), but my instinct is the savings are small change. But I haven't gone through the same level of analysis on it as I did withReflect.new
, so I'm open to being proved wrong on that.Granted using them would be simpler if you didn't care whether you're calling a constructor vs. non-constructor (e.g., you "just" use
call
/apply
). But is it really the case that there's a lot of code that doesn't need to care? Calls and construction are very different things.But a win for overloading
call
/apply
is, of course, that ES5 constructor code can subclass an ES2015+ class constructor without branching. Existing ES5 code could be fed an ES2015+ class constructor and (probably) Just Work™. But I don't know that it's a common use case outside DI frameworks, which are relatively few and thus upgradeable. -
Having
call
/apply
not work the same way a function call does (modulo their actual purpose) is a significant departure from how they've worked since they were added to JavaScript originally. Now every explanation of them must not only say "They call the function setting a specific value forthis
" but also "and they let you bypass the restriction preventing calling constructors as functions." Okay, so the counter-argument could be that allowing them to call a constructor as a function is a by-product of their main purpose (settingthis
), but to me it's both different and surprising. -
Allowing
this
prior tosuper()
in a subclass constructor depending on how it's called makes it impossible for lint tools and such to flag that up early as an error. So they can't support the majority case where that's going to be an error at runtime.
Perhaps all of those can be addressed or dismissed. I don't want to
give the impression of being married to anything (not that my opinion
is particularly important anyway). But my gut says overloading
call
/apply
(thus allowing early this
binding) isn't markedly
simpler, but is more problematic.
Side note: In my outline of Reflect.new
, I should have flagged up
that we'd really want at least the Reflect.isConstructor
part of
this proposal
to be in place. DI systems would need to know what they were dealing
with. But (to me, anyway) the Reflect.isConstructor
part of that
proposal is non-controversial (Reflect.isCallable
has an open
question about what it should say for class constructors -- how
relevant!).
I feel like I've done a lot of talking on this; I'm going to hang back and just listen for a while. :-)
-- T.J.
Granted using them would be simpler if you didn't care whether you're calling a constructor vs. non-constructor (e.g., you "just" use
In ES5 the only thing that makes a constructor behave differently is
invoking it with new
. There's no difference between a constructor and a
regular function, there's just a different way to invoke it. So yeah - I
don't care.
Having
call
/apply
not work the same way a function call does (modulo their actual purpose) is a significant departure from how they've worked since they were added to JavaScript original
I see your point but I don't exactly agree. It's not that they work
differently, it's just that you aren't allowed to invoke once specific type
of function directly. (And again if I had my way, I'd just drop the 'no
new' restriction entirely for the sake of 100% backward compatibility
between class vs. prototype syntax -- but I don't personally care about the
new
thing).
I'd make the counterpoint that the way things are now, we have a much more
dramatic departure from the way things worked since the beginning: I can
create a function that I have no ability to access directly through any
means. There has never been a "black box" process in Javascript before, now
there is. I could completely simulate the prototype chain construction
process and rewire it any way I wanted, now I can't. This is a big
functional and philosophical change. But mostly - from a practical
standpoint - it means that "class" syntax is not backwards compatible with
prototype syntax. I think that most developers would expect that with the
exception of builtins that never worked correctly using the old
instantiation patterns, everything else should work interchangeably. You
should be able to write simple classes using simple inhertance patterns and
have class
just act as syntactic sugar, unless you're trying to do
something that wasn't possible before.
I get that a class is not supposed to be exactly the same as a prototype constructor, since one of the goals was to fix the problem extending builtins, but it should be backwards compatible. Right now it's not in a very significant way.
The semantics of
super()
would need to be addressed, in the sense that
it too would have to be permitted to use an existing context in this circumstance.
This is true. Not being familiar with the implementation details I can't
say how big a deal this is, but yes - the ability to pass a context down
through super
(the same as you did old-school by invoking the superclass
constructor directly against your own context from within the subclass
constructor) is required. To me, this is just backwards compatibility.
Thanks again for your time and engagement here. This is a pretty important issue to me, and I'm definitely not knowledgable about the implemenetation details. I am very gratified to be able to get into it here and learn more.
I am brand new to this list, I find myself here because of a confounding issue related to ES6 classes vs. traditional constructors. Forgive me if this is something that's been hashed out in times past. I looked around for discussion online and couldn't find anything more than the observation that the spec prohibits invoking it - not really any discussion. Probably a failing of google more than anything else, so if there's some discussion that I should read to catch up please point me there.
Here's my issue. The ES6 spec prohibits invoking class constructors without "new". This makes such functions a special case, e.g.
class Test() {} // typeof Test === 'function' // yep // Test.prototype.constructor === Test // yep // Test() => nope ... TypeError: Class constructor Test cannot be invoked without 'new' // Test.call() ... nope // Test.apply() ... nope
This has some interesting consequences. It means testing something for typeof "function" no longer guarantees it can be invoked without error. Also "function.toString()" can now return something that isn't actually a legal function definiton (since it returns the whole class as text). There seems to be no method, through various javascript reflection/invocation techniques or otherwise, to invoke a class constructor except by creating an instance of the class.
For tool-builders the consequences of this are significant. It's no longer possible to create something that can extend/wrap/act on a prototype by intercepting it's construction process, as it was before with plain ES5 constructors. So classes are fundamentally different than prototype contructors in how we can use them, far more than syntactic sugar. This has come into play lately for me, as an DI container we use that does exactly this doesn't work with ES6 classes (and as far as I can tell, there's no way to make it work, other than having devs no longer use class syntax).
This seems a strange design decision. Even conventional OO languages like C# have the capability to reflect on classes and access the constructor directly as a function. It seems to fly in the face of the basic openness/dyanamic nature of JavaScript, and more signficantly, creates a kind of backward incompatibility since a function is no longer just a function.
I'm wondering whether I'm missing some mechanism for legally accessing a class constructor as a function (other than parsing the output of toString() and eval!) -- and generally thoughts on this aspect of the ES6 specification.
Thank you!