Determine if a value is Callable/Constructible
Last thread: esdiscuss.org/topic/add-reflect-isconstructor-and-reflect-iscallable. Died off again.
There's no issue with ES7 vs. ES6 without a spec, and having a draft spec at the right stage (1 for flagged implementation?) is the thing. Who will do it?
Well just to get the ball rolling, I’ve put together a markdown doc for this caitp/TC39-Proposals/blob/master/tc39-reflect-isconstructor-iscallable.md, caitp/TC39-Proposals/blob/master/tc39-reflect-isconstructor-iscallable.md
It’s such a minor item that I’m not sure how much to add to it, so maybe someone else will have a go at it instead.
(sorry for the spam)
Caitlin Potter wrote:
Well just to get the ball rolling, I’ve put together a markdown doc for this caitp/TC39-Proposals/blob/master/tc39-reflect-isconstructor-iscallable.md
It’s such a minor item that I’m not sure how much to add to it, so maybe someone else will have a go at it instead.
Thanks, truly appreciated. These should get advanced at the next meeting. We can jump them more than one step, to high stage even, after everyone has a look and a night to sleep on them. If everyone on TC39 takes a look now, even better.
On Mar 29, 2015, at 11:51 PM, Caitlin Potter <caitpotter88 at gmail.com> wrote:
...
Reflect.isConstructor(fn) -> true if Class constructor, generator, or legacy (and non-builtin) function syntactic form Reflect.isCallable(fn) -> true for pretty much any function, except for class constructors and a few builtins
I’ve already seen another situation (node’s Buffer) where code could be simplified by using a ES6 class definition but where that is prevented because a class constructor throws when called.
Just to clarify something. Class constructors actually are “callable”. You can observe this by the fact that Proxy allows you to install an “apply” handler (the reification of the [[[Call]] internal method) on a class constructor. The the fact that an object can be [[Call]]’ed is already reflected by the typeof operator. Class constructors throw when called because at the last minute we choose to make their [[Call]] do an explicit throw not because they aren’t callable.
There is no intrinsic reason why we needed to mandate that class constructors should throw when called. We even provided a simple and straight forward way (new.target===undefined) that a ES constructor body can use to determine whether it was called or new’ed.
I think we should just drop that throws when called feature of class constructors..
(The restriction was added to future proof for the possibility of inventing some other way to provide a class with distinct new/call behavior. I don’t think we need nor can afford to wait for the invention of a new mechanism which will inevitably be more complex than new.target, which we already have.)
On Sun, Mar 29, 2015 at 10:49 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Mar 29, 2015, at 11:51 PM, Caitlin Potter <caitpotter88 at gmail.com> wrote:
...
Reflect.isConstructor(fn) -> true if Class constructor, generator, or legacy (and non-builtin) function syntactic form Reflect.isCallable(fn) -> true for pretty much any function, except for class constructors and a few builtins
I’ve already seen another situation (node’s Buffer) where code could be simplified by using a ES6 class definition but where that is prevented because a class constructor throws when called.
Just to clarify something. Class constructors actually are “callable”. You can observe this by the fact that Proxy allows you to install an “apply” handler (the reification of the [[[Call]] internal method) on a class constructor. The the fact that an object can be [[Call]]’ed is already reflected by the typeof operator. Class constructors throw when called because at the last minute we choose to make their [[Call]] do an explicit throw not because they aren’t callable.
There is no intrinsic reason why we needed to mandate that class constructors should throw when called. We even provided a simple and straight forward way (new.target===undefined) that a ES constructor body can use to determine whether it was called or new’ed.
I think we should just drop that throws when called feature of class constructors..
(The restriction was added to future proof for the possibility of inventing some other way to provide a class with distinct new/call behavior. I don’t think we need nor can afford to wait for the invention of a new mechanism which will inevitably be more complex than new.target, which we already have.)
I don't think this is an accurate representation of the discussion we had.
It doesn’t seem that big of a deal, but one risk is: people mistaking a class for a constructor, trying to subclass it as if it were a constructor and things failing silently.
There is no intrinsic reason why we needed to mandate that class constructors should throw when called. We even provided a simple and straight forward way (new.target===undefined) that a ES constructor body can use to determine whether it was called or new’ed.
I think we should just drop that throws when called feature of class constructors..
(The restriction was added to future proof for the possibility of inventing some other way to provide a class with distinct new/call behavior. I don’t think we need nor can afford to wait for the invention of a new mechanism which will inevitably be more complex than new.target, which we already have.)
I don't think this is an accurate representation of the discussion we had.
Any other reasons for throwing? It’d be great to know what they were!
On Mar 30, 2015, at 8:09 AM, Yehuda Katz <wycats at gmail.com> wrote:
...
I don't think this is an accurate representation of the discussion we had.
It’s my characterization of the situation and reflects my position. I agreed to disabling calling class constructors via a throw in order to get the consensus necessary to move forward with finishing ES6. However, I also think that that restriction was technically unnecessarily and crippling for some use cases. I know you have some, as yet not fully specified, alternative in mind. I don’t know its details so I can’t directly comment on it. But, I’m skeptical that of the need for anything other than new.target and I’m pretty sure that any alternative will be move complex and take longer to get into implementations.
On Mar 30, 2015, at 8:40 AM, Axel Rauschmayer <axel at rauschma.de> wrote:
It doesn’t seem that big of a deal, but one risk is: people mistaking a class for a constructor, trying to subclass it as if it were a constructor and things failing silently.
Can you give an example of what you mean?
It doesn’t seem that big of a deal, but one risk is: people mistaking a class for a constructor, trying to subclass it as if it were a constructor and things failing silently.
Can you give an example of what you mean?
class MySuperClass {}
// This function assumes that MySuperClass is an ES5 constructor function
function MySubConstructor(foo) {
MySuperClass.call(this);
this.foo = foo;
}
MySubConstructor.prototype = Object.create(MySuperClass.prototype);
MySubConstructor.prototype.constructor = MySubConstructor;
On Mar 30, 2015, at 10:12 AM, Axel Rauschmayer <axel at rauschma.de> wrote:
It doesn’t seem that big of a deal, but one risk is: people mistaking a class for a constructor, trying to subclass it as if it were a constructor and things failing silently.
Can you give an example of what you mean?
class MySuperClass {} // This function assumes that MySuperClass is an ES5 constructor function function MySubConstructor(foo) { MySuperClass.call(this); this.foo = foo; } MySubConstructor.prototype = Object.create(MySuperClass.prototype); MySubConstructor.prototype.constructor = MySubConstructor;
so if MySuperCall didn’t have the throw on [[Call]] behavior the above would work just fine.
class MySuperClass {} // This function assumes that MySuperClass is an ES5 constructor function function MySubConstructor(foo) { MySuperClass.call(this); this.foo = foo; } MySubConstructor.prototype = Object.create(MySuperClass.prototype); MySubConstructor.prototype.constructor = MySubConstructor;
so if MySuperCall didn’t have the throw on [[Call]] behavior the above would work just fine.
In general, I’d expect this kind of subclassing to fail, due to the new instantiation protocol. Wrong?
On Mar 30, 2015 10:54 AM, Axel Rauschmayer <axel at rauschma.de> wrote:
class MySuperClass {} // This function assumes that MySuperClass is an ES5 constructor function function MySubConstructor(foo) { MySuperClass.call(this); this.foo = foo; } MySubConstructor.prototype = Object.create(MySuperClass.prototype); MySubConstructor.prototype.constructor = MySubConstructor;
so if MySuperCall didn’t have the throw on [[Call]] behavior the above would work just fine.
In general, I’d expect this kind of subclassing to fail, due to the new instantiation protocol. Wrong?
Would work fine if throw semantics removed and invoked as
new MySubConstructor();
Just like ES5.
If invoked as
MySubConstructor();
would fail just like ES5.
Throwing on [[Call]] is a compatibility hazard.
Don’t the different assumptions as to where the instance is allocated ever clash here? What if MySuperClass
were:
class MySuperClass extends Error {
}
Le 30 mars 2015 à 10:46, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
On Mar 30, 2015, at 10:12 AM, Axel Rauschmayer <axel at rauschma.de> wrote:
It doesn’t seem that big of a deal, but one risk is: people mistaking a class for a constructor, trying to subclass it as if it were a constructor and things failing silently.
Can you give an example of what you mean?
class MySuperClass {} // This function assumes that MySuperClass is an ES5 constructor function function MySubConstructor(foo) { MySuperClass.call(this); this.foo = foo; } MySubConstructor.prototype = Object.create(MySuperClass.prototype); MySubConstructor.prototype.constructor = MySubConstructor;
so if MySuperCall didn’t have the throw on [[Call]] behavior the above would work just fine.
Allen
I see an issue when MySuperClass contains itself a super() invocation... at least when that super-class is some builtin that doesn't support initialisation of pre-allocated instances, like Array
. As currently specified, it will just throw, which is at least safe. It would be interesting to make it just work without hacks such as if (new.target) super(); else super.constructor(...);
, and, in the same time, without silently break with Array
, etc.
On Mar 30, 2015, at 10:12 AM, Axel Rauschmayer <axel at rauschma.de>
wrote:
It doesn’t seem that big of a deal, but one risk is: people mistaking
a class for a constructor, trying to subclass it as if it were a constructor and things failing silently.
Can you give an example of what you mean?
class MySuperClass {} // This function assumes that MySuperClass is an ES5 constructor function function MySubConstructor(foo) { MySuperClass.call(this); this.foo = foo; } MySubConstructor.prototype = Object.create(MySuperClass.prototype); MySubConstructor.prototype.constructor = MySubConstructor;
so if MySuperCall didn’t have the throw on [[Call]] behavior the above
would work just fine.
Allen
Would this be a work around? i'm seeing this work with v8 harmony classes enabled, or is this something that wont work eventually?
function MySubConstructor(foo) {
MySuperClass.constructor.call(this)
this.foo = foo;
}
I also noticed that using apply to chain constructors won't work either as currently mentioned at Mozilla Using apply to chain constructors
//
function applyConstructor(ctor, args) {
var child = Object.create(ctor.prototype);
var result = ctor.apply(child, args); // !!! throws Class constructors
cannot be invoked without 'new'
return result && Object(result) === result ? result : child;
}
but this still seems to work
function applyConstructor(ctor, args) {
return new (Function.prototype.bind.apply(ctor, [null].concat(args)));
}
On Mar 30, 2015, at 1:49 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
There is no intrinsic reason why we needed to mandate that class constructors should throw when called. We even provided a simple and straight forward way (new.target===undefined) that a ES constructor body can use to determine whether it was called or new’ed.
I don’t think it’s great to have branches in a constructor dealing with this — it’s not super-obvious reading the code what it means (so it’s another thing to train people to understand).
A better way (which I think has been suggested by someone already in a different thread), would be to have a separate “magic” method to provide call
code.
class Buffer {
constructor(…a) {
// …
}
factory(…a) { // [@@factory](), __factory__(), whatever
return new Buffer(…a);
// Or whatever else one might wish to do in a factory method
}
}
But, I think the factory problem is solved well enough with static methods
class Buffer {
constructor(…a) {
this.initialize(…a);
}
// Much easier to understand these, compared with Buffer(someBuffer) or Buffer(someArray) etc
static withBuffer(buffer) { assert(Buffer.isBuffer(buffer)); return new Buffer(buffer); }
static withArray(array) { assert(Array.isArray(array)); return new Buffer(array); }
static withSize(size) { assert(IsUInt(size)); return new Buffer(size); }
static fromString(str, encoding = “utf8") { assert(IsString(str) && IsString(encoding)); return new Buffer(str, encoding); }
initialize(…a) {
switch (a.length) {
case 1:
if (IsUInt(a[0])) return allocateBufferOfSize(this, a[0]);
else if (Array.isArray(a[0]) return allocateBufferFromArray(this, a[0]);
else if (Buffer.isBuffer(a[0]) return allocateCopyOfBuffer(this, a[0]);
else if (IsString(a[0]) { /* fall through */ }
else ThrowTypeError(“Function called with incorrect arguments!");
case 2:
if (IsUndefined(a[1]) a[1] = “utf8”;
if (IsString(a[0] && IsString(a[1])) return allocateBufferFromString(this, a[0], a[1]);
default:
ThrowTypeError(“Function called with incorrect arguments!");
}
}
}
I think we should just drop that throws when called feature of class constructors..
(The restriction was added to future proof for the possibility of inventing some other way to provide a class with distinct new/call behavior. I don’t think we need nor can afford to wait for the invention of a new mechanism which will inevitably be more complex than new.target, which we already have.)
I’m all for it if it can be allowed without making classes more complicated for consumers to use — The thing I like about requiring new
is that it’s very simple and straight forward.
But in either case, these (IsCallable / IsConstructor) are pretty basic qualities of objects that a Reflection* api ought to be able to read into, imho.
Don’t the different assumptions as to where the instance is allocated ever clash here? What if
MySuperClass
were:
class MySuperClass extends Error { }
MySubClass preallocates when invoked via new. Just like ES5. So,
MySuperClass.call(this)
is same as ES5. What happens in MySuperClass depends upon the ES6 level programmer.
Right, but I don’t see how an ES5-style constructor MySubContructor can allocate its instance and then have it initialized by an ES6 class (where the instance is allocated by a super-class). This is about ES5 code being confronted with ES6 code and assuming to see a constructor.
Thanks Caitlin for actually putting this onto github! I wasn't aware of that process when I posted about this to the mailinglist. Asking again from last time: Should we have Type == Object check like Reflect.isExtensible?
On Mon, Mar 30, 2015 at 5:36 AM, Caitlin Potter <caitpotter88 at gmail.com>
wrote:
On Mar 30, 2015, at 1:49 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
There is no intrinsic reason why we needed to mandate that class constructors should throw when called. We even provided a simple and straight forward way (new.target===undefined) that a ES constructor body can use to determine whether it was called or new’ed.
I don’t think it’s great to have branches in a constructor dealing with this — it’s not super-obvious reading the code what it means (so it’s another thing to train people to understand).
That's exactly my position. The co-mingling of [[Construct]] and [[Call]] in a single function was a side-effect of having all-singing, all-dancing functions. The nice thing about ES6 is that we got dedicated syntax for classes and callbacks. Just because we can make constructor functions serve double-duty via a reflective mechanism doesn't mean that's the right thing to do.
A better way (which I think has been suggested by someone already in a different thread), would be to have a separate “magic” method to provide
call
code.
class Buffer { constructor(…a) { // … } factory(…a) { // [@@factory](), __factory__(), whatever return new Buffer(…a); // Or whatever else one might wish to do in a factory method } }
That was the proposal I made that Allen alluded to:
class Buffer {
constructor(…a) {
// …
}
[Symbol.call](a) {
if (typeof a === 'string') {
return Buffer.fromString(a);
}
return new Buffer(…arguments);
}
}
But, I think the factory problem is solved well enough with static methods
class Buffer { constructor(…a) { this.initialize(…a); } // Much easier to understand these, compared with Buffer(someBuffer) or Buffer(someArray) etc static withBuffer(buffer) { assert(Buffer.isBuffer(buffer)); return new Buffer(buffer); } static withArray(array) { assert(Array.isArray(array)); return new Buffer(array); } static withSize(size) { assert(IsUInt(size)); return new Buffer(size); } static fromString(str, encoding = “utf8") { assert(IsString(str) && IsString(encoding)); return new Buffer(str, encoding); } initialize(…a) { switch (a.length) { case 1: if (IsUInt(a[0])) return allocateBufferOfSize(this, a[0]); else if (Array.isArray(a[0]) return allocateBufferFromArray(this, a[0]); else if (Buffer.isBuffer(a[0]) return allocateCopyOfBuffer(this, a[0]); else if (IsString(a[0]) { /* fall through */ } else ThrowTypeError(“Function called with incorrect arguments!"); case 2: if (IsUndefined(a[1]) a[1] = “utf8”; if (IsString(a[0] && IsString(a[1])) return allocateBufferFromString(this, a[0], a[1]); default: ThrowTypeError(“Function called with incorrect arguments!"); } } }
I agree that static methods are sufficient, but I also agree that it would be nice to be able to describe existing built-in APIs in terms of classes. That doesn't, however, mean that we need to force both use-cases into a single function called constructor.
I feel strongly that this:
class Buffer {
constructor(from) {
// switch on Number, isArray, or Buffer
}
[Symbol.call](from, encoding='utf8') {
if (typeof from === 'string') {
return Buffer.fromString(from, encoding);
}
return new Buffer(from);
}
}
is clearer than:
class Buffer {
constructor(from, encoding='utf8') {
if (!new.target) {
if (typeof from === 'string') {
return Buffer.fromString(from, encoding);
}
}
// switch on Number, isArray, or Buffer
}
}
For one thing, it requires the reader to know that new.target
is being
used to determine whether the constructor was called with new
. While it
certainly is expressive enough, it's an unusual reflective operation that
doesn't exactly say what you mean. For another, putting two uses into a
single method and separating them by an if
is quite often a hint that you
want to break things up into two methods. I think that's the case here.
One of the nice things about the [Symbol.call]
method is that a reader of
the class can determine at a glance whether it handles [[Call]], and not
have to scan the constructor to see if (and how!) new.target
is used. And
since new.target
can also be used for other usages, a reader unfamiliar
with the pattern might not even have a good query to Google (searching
"what is new.target for in JavaScript", even if that works at all, might
likely bring up a bunch of articles about implementing base classes).
I think we should just drop that throws when called feature of class constructors..
(The restriction was added to future proof for the possibility of inventing some other way to provide a class with distinct new/call behavior. I don’t think we need nor can afford to wait for the invention of a new mechanism which will inevitably be more complex than new.target, which we already have.)
I'll bring up [Symbol.call]
at the next meeting. It would be quite
helpful if you would enumerate the areas in which you expect it to be
complex, so I can make sure to address them in my proposal.
I’m all for it if it can be allowed without making classes more complicated for consumers to use — The thing I like about requiring
new
is that it’s very simple and straight forward.
The reason we dropped it was precisely because several of us felt that the
cryptic if (new.target)
check was a throwback to the original all-in-one
design of functions, and that the new class syntax gives us the breathing
room we need to describe things in a clear way without losing
expressiveness.
But in either case, these (IsCallable / IsConstructor) are pretty basic qualities of objects that a Reflection* api ought to be able to read into, imho.
What Allen is saying is that the implementation of "throw if constructor" doesn't work by not implementing [[Call]], but rather by implementing [[Call]] to throw, so those reflective APIs would say the wrong thing, and that this is observable via proxies.
Allen, can you say more about why you spec'ed it that way?
I don’t think [Symbol.call] is a very good mechanism. A new syntactic form would make more sense.
It doesn’t seem right that you should have to introduce an observable prototype property just to get [[Call]] behavior. (Or a constructor property, if you change the syntax to static [Symbol.call]() { … }
.)
And it raises the question of what happens when I add a [Symbol.call] property to other objects. What does it even mean? If I do var obj = { prototype: { [Symbol.call]() { console.log("foo"); } } }
, can I now call obj()
? Is typeof obj === "function"
? Very strange stuff. Presumably not very VM-friendly either, but that's just a guess.
Great to get the ball rolling. Here's some issues I'd bring up at the meeting, so we can get a head start:
-
As discussed recently in esdiscuss.org/topic/reflect-getownpropertysymbols, right now Reflect only holds counterparts to the proxy traps. I think this is kind of a nice property. Do we want to expand Reflect into a dumping ground for all "reflective" operations? That's been implied many times on the list, but so far hasn't happened, from what I can see. On the other hand, I don't have any good ideas for where else to put these things. (Maybe Function.isConstructor? Not sure it scales.)
-
Reflect.isCallable seems pretty pointless, when you can just do
typeof x === "function"
. YAGNI IMO. Note that it also doesn't match your earlier description from esdiscuss.org/topic/determine-if-a-value-is-callable-constructible#content-0, so it presumably doesn't solve those use cases. (But that definition can't really be workable, since all of the things you mention as not being callable are actually callable, in the same wayfunction f() { throw new TypeError(); }
is callable.)
But in either case, these (IsCallable / IsConstructor) are pretty basic qualities of objects that a Reflection* api ought to be able to read into, imho.
What Allen is saying is that the implementation of "throw if constructor" doesn't work by not implementing [[Call]], but rather by implementing [[Call]] to throw, so those reflective APIs would say the wrong thing, and that this is observable via proxies.
It’s a fair point, constructors do have a [[Call]] internal method — there’s no reason this needs to be described this way though. Instead of the extra step for classConstructors in 9.2.1, class constructors could just as easily not have a [[Call]] method at all (by default). I guess what I’m getting at is, right now they aren’t “really” callable, it’s just that the way their non-callable-ness is expressed makes them appear callable (which probably should not be the case). Since they’re intrinsically not callable (currently), they shouldn’t be treated as callable.
What Domenic is saying later on makes sense, the magic “call-code” method (should it ever exist) shouldn’t be a property of the class prototype, so a special syntactic form would work better (but also be kind of awful, too).
If you made them non-callable by not implementing [[Call]], then typeof would no longer return "function", which would be ... O_o.
If you made them non-callable by not implementing [[Call]], then typeof would no longer return "function", which would be ... O_o.
s/Object (implements [[Call]]) | “function” /Object (implements [[Call]] or [[FunctionKind]] is classConstructor) | “function”/g
Problem solved!
I jest — but making a constructor identifiable as a function while still noting that it’s not “really” callable is certainly possible
It doesn’t seem right that you should have to introduce an observable prototype property just to get [[Call]] behavior. (Or a constructor property, if you change the syntax to
static [Symbol.call]() { … }
.)And it raises the question of what happens when I add a [Symbol.call] property to other objects. What does it even mean? If I do
var obj = { prototype: { [Symbol.call]() { console.log("foo"); } } }
, can I now callobj()
? Istypeof obj === "function"
? Very strange stuff. Presumably not very VM-friendly either, but that's just a guess.
I'd imagine that you'd re-spec [[Call]] for class constructors to basically
do this[Symbol.call](...args)
instead of just throw. It would therefore
only have an effect within class constructors. Is that still weird?
Yehuda Katz (ph) 718.877.1325
On Mon, Mar 30, 2015 at 8:09 AM, Domenic Denicola <d at domenic.me> wrote:
I don’t think [Symbol.call] is a very good mechanism. A new syntactic form would make more sense.
It doesn’t seem right that you should have to introduce an observable prototype property just to get [[Call]] behavior. (Or a constructor property, if you change the syntax to
static [Symbol.call]() { … }
.)
A new syntactic form sounds good to me :)
From: Kevin Smith [mailto:zenparsing at gmail.com]
I'd imagine that you'd re-spec [[Call]] for class constructors to basically do
this[Symbol.call](...args)
instead of just throw. It would therefore only have an effect within class constructors. Is that still weird?
At least it's explicable, but it's still pretty weird I think. I mean, we don't specify [[Construct]] by saying that it does new this.prototype.constructor(...args)
or similar. The asymmetry is jarring.
And it's weird to have this symbol with such a generic name that doesn't work for anything except class syntax. I'd expect symbols to be for re-usable protocols... that's fuzzy intuition though, I admit, and might be contradicted by existing examples.
On Mon, Mar 30, 2015 at 8:54 AM, Domenic Denicola <d at domenic.me> wrote:
From: Kevin Smith [mailto:zenparsing at gmail.com]
I'd imagine that you'd re-spec [[Call]] for class constructors to basically do
this[Symbol.call](...args)
instead of just throw. It would therefore only have an effect within class constructors. Is that still weird?At least it's explicable, but it's still pretty weird I think. I mean, we don't specify [[Construct]] by saying that it does
new this.prototype.constructor(...args)
or similar. The asymmetry is jarring.And it's weird to have this symbol with such a generic name that doesn't work for anything except class syntax. I'd expect symbols to be for re-usable protocols... that's fuzzy intuition though, I admit, and might be contradicted by existing examples.
I like the idea of a special syntactic form a lot. One of the nice things
about constructor
is that it's easy to explain "you [[Construct]] with
the constructor". We can't use call
similarly any more, but I totally
agree something like it would be pretty nice.
On the flip side, it's not entirely clear that allowing people to
override [[Call]] on an existing function is a no-go. Changing the typeof
via installing a symbol definitely seems like bad juju though.
If you made them non-callable by not implementing [[Call]], then typeof would no longer return "function", which would be ... O_o.
In some way, that would even be correct – depending on what you expect a function to be: something callable or something constructible. Alas, there is currently no simple way to distinguish “callable” and “constructible”.
I like the idea of a special syntactic form a lot. One of the nice things about
constructor
is that it's easy to explain "you [[Construct]] with the constructor". We can't usecall
similarly any more, but I totally agree something like it would be pretty nice.
Unfortunately, we can't use a magic method name. Just to throw out a couple of other ideas though:
Parens sans method name:
class C {
constructor() { /* new me */ }
() { /* call me */ }
}
Contextual keyword after constructor:
class C {
constructor() { /* new me */ }
constructor call() { /* call me */ }
}
With a "dot" instead:
class C {
constructor() { /* new me */ }
constructor.call() { /* call me */ }
}
From: Axel Rauschmayer [mailto:axel at rauschma.de]
In some way, that would even be correct – depending on what you expect a function to be: something callable or something constructible. Alas, there is currently no simple way to distinguish “callable” and “constructible”.
Why do you say they're not callable?
Is this function callable?
function f() {
throw new TypeError("cannot call f because you're not pretty enough");
}
what about
function f() {
throw new TypeError("Class constructors cannot be invoked without 'new'");
}
?
Unfortunately, we can't use a magic method name. Just to throw out a couple of other ideas though:
Contextual keyword after constructor:
class C { constructor() { /* new me / } constructor call() { / call me */ } }
This one looks like the most readable — I would make a slight adjustment:
class C {
constructor() { /* new me *. }
call constructor() { /* call me */ }
}
(just for the purpose of readability)
I think it might be hard to mix this up with computed property names, so call [‘constructor’]()
might be a no-go? same with [‘constructor’] call()
tbh
On Mar 30, 2015, at 12:20 PM, Domenic Denicola <d at domenic.me> wrote:
Why do you say they're not callable?
Is this function callable?
function f() { throw new TypeError(“f immediately throws"); }
I think there’s a distinction here.
function f() {}
might throw when called, but that’s in the author of the function’s control.
class C {}
will always throw when called (in the current draft), so you can’t “really” consider it callable. No user-authored code is invoked when [[Call]]-ing a class constructor
In some way, that would even be correct – depending on what you expect a function to be: something callable or something constructible. Alas, there is currently no simple way to distinguish “callable” and “constructible”.
Why do you say they're not callable?
As in “makes sense to call”. In the past, typeof x === 'function'” was an adequate test for checking whether it makes sense to call
x`. Since ES6, it isn’t, anymore. That’s why the checks proposed by Caitlin will be nice to have.
From: Axel Rauschmayer [mailto:axel at rauschma.de]
As in “makes sense to call”. In the past,
typeof x === 'function'” was an adequate test for checking whether it makes sense to call
x`.
I tried to explain explicitly why I don't think this is true.
Since ES6, it isn’t, anymore.
I disagree. The situation has not changed at all.
On Mar 30, 2015, at 1:44 PM, Domenic Denicola <d at domenic.me> wrote:
From: Axel Rauschmayer [mailto:axel at rauschma.de] As in “makes sense to call”. In the past,
typeof x === 'function'” was an adequate test for checking whether it makes sense to call
x`.
I> tried to explain explicitly why I don't think this is true.
Since ES6, it isn’t, anymore.
I disagree. The situation has not changed at all.
As I said above:
var called = false;
function F() {
called = true;
throw new TypeError(“can’t call F()”);
}
F(); // Throws, but `called` is true
called = false;
class C {
constructor() {
called = true;
throw new TypeError(“can’t call C()”);
}
}
C(); // Throws, but `called` is false, because the constructor was never invoked
That distinction means that you can’t “really” [[Call]] a constructor, it’s just that the reason is slightly different from trying to [[Call]] an Array instance (or something)
The distinction you're really pointing to here is the distinction between user-generated throwing functions and runtime-generated ones. Both are called. User-generated ones you could edit the source code of and insert a called = true
line first, whereas runtime-generated ones you could not. But that's not a real material difference in call-ability.
I think we’re on the same page that the runtime is the one doing the throwing, but that doesn’t really matter. The point is, if you can’t actually invoke the authored code, you can’t really call the authored code “call-able”. To the VM, the difference doesn’t matter a whole lot, but to human beings, it does. It’s disingenuous to call it “call-able” if the authored code can’t be “called”. (A proxy trap doesn’t really make it “callable” either, it just allows different code to be “called” in its place).
It could still be identified as a “function” for compat with ES5, but the behaviour is different from any other sort of function, it should be identifiable as different.
It could still be identified as a “function” for compat with ES5, but the behaviour is different from any other sort of function, it should be identifiable as different.
Right, but again, I don't think the behavior is any different from function f() { throw new TypeError(); }
, so whatever the test you write returns for classes, my position is that it should return the same thing for f
.
That distinction means that you can’t “really” [[Call]] a constructor, it’s just that the reason is slightly different from trying to [[Call]] an Array instance (or something)
Another way to look at the situation is that, by default, calling a class constructor will throw an error. That default may be changed however. Just not with ES6 : )
On 3/30/15 1:33 PM, Axel Rauschmayer wrote:
As in “makes sense to call”. In the past,
typeof x === 'function'” was an adequate test for checking whether it makes sense to call
x`.
Except for all the DOM constructors, right?
I personally think it's better to have harmony between ES5 and ES6 classes and not incompatibility. The web and the like are currently littered with applications that use [[call]] as a sub classing technique and now all these apps will be give a harsh choice, either disregard using ES6 class syntax, completely overhaul to suit ES6 or just don't move to ES6 until time\money budgets permit.
Is it not possible to:
- Have a unified construct for both class and function syntax, i.e.
SomeClass.constructor.call
can be used to sub class for both ES6 and ES6 - Instead of throwing the message
Class constructors cannot be invoked without 'new'
change it to becomeClass constructors cannot be invoked without using 'new' or it's .constructor method
- Functions will continue to have [[call]] regardless but it should
still be recommended to use
SomeClass.constructor.call
for sub classing for great compatibility - Polyfill vendors have their own spec for this scenario. i.e. when
the detect class syntax is used then mark the output ES5 prototype
with a [[class-tag]] that allows them to assert and throw the same
Class constructors cannot be invoked.....
message as stated in point 2 above
disclaimer I know this has been brought up before, but bump :>
People are experimenting with polyfilled class implementations, which don’t all correctly throw when called as a function (no
new
). Eventually, they’re likely to be disappointed that this isn’t legal, and might have to undergo some serious pains to fix their applications.I notice that this is particularly problematic for AngularJS, because classes are registered with an injector, which doesn’t know if it can
[[Call]]
them or not. It will later on try to[[Call]]
(depending on how the class was registered with DI). It would be really great if we had a way to determine if this was going to throw or not, other than looking at the stringified value of a function, so that these libraries could be updated to accomodate new class behaviour without pains (try/catch or processing Function.toString())Some ideas:
Reflect.isConstructor(fn) -> true if Class constructor, generator, or legacy (and non-builtin) function syntactic form
Reflect.isCallable(fn) -> true for pretty much any function, except for class constructors and a few builtins
I know it’s way too late for ES6, but maybe some kind of fast-tracked extension is in order? it should be pretty simple to implement these (and SM and v8 have variations of these in the runtime anyways)