IsConstructor

# Domenic Denicola (11 years ago)

A variety of places in the spec use the new IsConstructor abstract operation. In ES5, this test (essentially, "does the object implement the [[Construct]] internal method") was restricted to the new operator. But in ES6, it is used in implementing a large variety of built-in functions:

  • All Array methods
  • All %TypedArray% methods
  • All Promise methods (via NewPromiseCapability)

(Note that there are two uses: arrays and typed arrays do alternative logic for non-constructors; promises fail immediately. This inconsistency might be a bug?)

It seems to me that we should expose this primitive reflective operation to users, instead of having all of these methods be able to detect something that user code can't, and thus making them harder to explain or polyfill.

Alternately, if we don't think users should be doing this kind of reflection, then we probably shouldn't be doing it ourselves. In which case, figuring out an alternate path for the above methods would be useful---perhaps they simply try to construct, and fail immediately if used with a non-constructible object, instead of falling back.

WDYT?

# Erik Arvidsson (11 years ago)

Another way of thinking of IsConstructor is whether the function has an own prototype property or not. The only exception[*] there is bound functions where one would need to know if the [[TargetFunction]] IsConstructor or not.

[*] Proxies are oddballs here. All Proxies have a [[Construct]] method so the IsConstructor will always return true which is really not what you want. If IsConstructor was changed to check for a .prototype instead proxies would behave more inline with ordinary objects.

# Rick Waldron (11 years ago)

I had similar questions a couple years ago and Allen advised that the easiest polyfill for such a mechanism is:

function isConstructor(C) {
  try {
    new C();
    return true;
  } catch (e) {
    return false;
  }
}

Additionally, at the July 2012 tc39 meeting I proposed (over breakfast) an ES7 "standard library module" that exported the abstract operations that are now defined in chapter 7 people.mozilla.org/~jorendorff/es6-draft.html#sec-abstract-operations, the response was positive but it was far too early to have a serious discussion. Anyway, with that you'd just write:

import { isConstructor } from "es-abstract";
# André Bargull (11 years ago)

[*] Proxies are oddballs here. All Proxies have a [[Construct]] method so the IsConstructor will always return true which is really not what you want. If IsConstructor was changed to check for a .prototype instead proxies would behave more inline with ordinary objects.

[[Construct]] is only attached to a proxy if the target has a [[Construct]] internal method.

# Rick Waldron (11 years ago)

Quick note: that isConstructor isn't really viable unless you plan on using it with constructors that do not have side effects.

# Boris Zbarsky (11 years ago)

On 6/11/14, 10:58 AM, Rick Waldron wrote:

function isConstructor(C) {
   try {
     new C();

This will fail for constructors that require actual arguments, right?

For example, this will fail if C === window.Worker in a browser.

# Rick Waldron (11 years ago)

On Wed, Jun 11, 2014 at 11:05 AM, Boris Zbarsky <bzbarsky at mit.edu> wrote:

This will fail for constructors that require actual arguments, right?

This, as well as the other warning I posted.

# André Bargull (11 years ago)

Quick note: that isConstructor isn't really viable unless you plan on using it with constructors that do not have side effects.

The Proxy-based solution needs to be used in these cases. Now we just need to wait until Proxies are available everywhere! ;-)

# Tom Van Cutsem (11 years ago)

2014-06-11 16:48 GMT+02:00 Erik Arvidsson <erik.arvidsson at gmail.com>:

[*] Proxies are oddballs here. All Proxies have a [[Construct]] method so the IsConstructor will always return true which is really not what you want. If IsConstructor was changed to check for a .prototype instead proxies would behave more inline with ordinary objects.

No, only proxies whose target has a [[Construct]] method will themselves have a [[Construct]] method. IOW, proxies should be completely transparent w.r.t. the IsConstructor test. See 9.5.15 ProxyCreate step 5.b.

I believe there may be a spec bug, as step 4 should explicitly exclude [[Construct]] and [[Call]], yet seems to include all methods defined in sec 9.5. Allen?

# C. Scott Ananian (11 years ago)

On Wed, Jun 11, 2014 at 11:05 AM, Boris Zbarsky <bzbarsky at mit.edu> wrote:

This will fail for constructors that require actual arguments, right?

For example, this will fail if C === window.Worker in a browser.

It will also fail for the uses of IsConstructor in the promise spec, as Promise constructors throw TypeError unless they are passed a callable resolve argument (IIRC).

# Alexandre Morgaut (11 years ago)

On 11 juin 2014, at 17:11, André Bargull <andre.bargull at udo.edu> wrote:

The Proxy-based solution needs to be used in these cases. Now we just need to wait until Proxies are available everywhere! ;-)

Not sure Proxy solve anything when we want to test the constructor nature of a function and be sure invoking it won't change anything in the current application context

To highlight what side-effects mentioned by Rick could be, a tested function while invoked may:

  • do DOM Manipulations,
  • change properties in the global object,
  • initiate Web Sockets or Web Workers...

Not safe at all...

Testing the prototype is also one thing, but constructors can have no prototype properties or methods but only own properties defined at the instance level (potentially with getters / setters) As created dynamically, such own properties are hard if not impossible to detect without executing the constructor

The only safe way I would see, for functions written in JS, would be to use a lexer on the source of the function provided by Function.prototype.toString.call(C)

But it would still not be 100% perfect and should probably throw an exception or return a error in some situations in which "this" manipulation may not be detectable. ex: the function code evaluate a dynamic strings (that can not be parsed by a lexer) via eval(), setTimeout()/ setInterval(), or new Function()

# André Bargull (11 years ago)

On 6/11/2014 5:40 PM, Alexandre Morgaut wrote:

Not sure Proxy solve anything when we want to test the constructor nature of a function and be sure invoking it won't change anything in the current application context

From 1:

function IsConstructor(o) {
   try {
     new (new Proxy(o, {construct: () => ({})}));
     return true;
   } catch(e) {
     return false;
   }
}

This IsConstructor implementation does not trigger any side-effects and works even when the underlying constructor requires arguments etc.

# Allen Wirfs-Brock (11 years ago)

On Jun 11, 2014, at 8:16 AM, Tom Van Cutsem wrote:

I believe there may be a spec bug, as step 4 should explicitly exclude [[Construct]] and [[Call]], yet seems to include all methods defined in sec 9.5. Allen?

Kind of boarder line. 6.1.7.2 says that the "essential internal methods" are those listed in Table 5 (does not include [[Call]] and [[Constructor]]). Also the definitions of [[Call]] and [[Construct]] in 9.5 each include a note that says these internal methods are only provided when a corresponding internal method exists on the target.

Reading the section as a while, I think it is pretty clear that that a Proxy only has a [[Construct]] internal method if its target also has one.

# Allen Wirfs-Brock (11 years ago)

On Jun 11, 2014, at 8:49 AM, André Bargull wrote:

From [1]:

function IsConstructor(o) {
 try {
   new (new Proxy(o, {construct: () => ({})}));
   return true;
 } catch(e) {
   return false;
 }
}

This IsConstructor implementation does not trigger any side-effects and works even when the underlying constructor requires arguments etc.

[1] anba/es6draft/blob/master/src/test/scripts/suite/lib/assert.js#L53-L60

+1.

I was just about to write and post the equivalent.

# Allen Wirfs-Brock (11 years ago)

On Jun 11, 2014, at 7:24 AM, Domenic Denicola wrote:

A variety of places in the spec use the new IsConstructor abstract operation. In ES5, this test (essentially, "does the object implement the [[Construct]] internal method") was restricted to the new operator. But in ES6, it is used in implementing a large variety of built-in functions:

  • All Array methods
  • All %TypedArray% methods
  • All Promise methods (via NewPromiseCapability)

(Note that there are two uses: arrays and typed arrays do alternative logic for non-constructors; promises fail immediately. This inconsistency might be a bug?)

Maybe you're looking at things differently from me, but I only see use of IsConstructor in a few Array methods, not all of them.

For most places in the spec. if you trace the logic of the algorithms that use IsConstructor it be being used as a guard on a [[Construct]] call and the alternative path is to throw a TypeError. In ES code this would be done automatically by the 'new' operator but must be done explicitly in the pseudo-code. A JS self-hosted implementation of those algorithm that used 'new' would not need to do an explicit IsConstructor test in most of those cases.

There are a few uses of IsConstructor in some Array methods that deal with subtle backwards compat issues that are a result of extending Array to be subclassable. These are very unique cases and I don't think you should look at them as establishing a pattern that should be followed in other situations.

Array.from and Array.of have a non-throwing IsConstructor test because they are designed to allow things like this:

let of = Array.of;
of(1,2,3,4,5);   //Equivalent to: Array.of(1,2,3,4,5)

I don't recall why we provided that functionality. It doesn't seem to me like a pattern we should want to encourage.

It seems to me that we should expose this primitive reflective operation to users, instead of having all of these methods be able to detect something that user code can't, and thus making them harder to explain or polyfill.

Most of these uses are simply emulating the behavior of the 'new' operator so it really shouldn't be needed to be tested explicitly in a polyfill. When it is really need, a user land IsConstructor test can be written using Proxy.

Alternately, if we don't think users should be doing this kind of reflection, then we probably shouldn't be doing it ourselves. In which case, figuring out an alternate path for the above methods would be useful---perhaps they simply try to construct, and fail immediately if used with a non-constructible object, instead of falling back.

WDYT?

Other than the Array.of and Array.from usages the other uses are all necessary either functionally (you can't just "try to construct" by calling [[Construct]], it requires an explicit guard) or to deal with special backwards compat situations.

I'd be happy to eliminate the special handling in Array.of and Array.from if there is consensus that it isn't desirable.

# C. Scott Ananian (11 years ago)

On Wed, Jun 11, 2014 at 12:44 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

Array.from and Array.of have a non-throwing IsConstrutor test because they are designed to allow things like this:

let of = Array.of;
of(1,2,3,4,5);   //Equivalent to: Array.of(1,2,3,4,5)

I don't recall why we provided that functionality. It doesn't seem to me like a pattern we should want to encourage.

Why not just require:

let of = Array.of.bind(Array);
of(1,2,3,4,5);

That seems like it would work just as well, and not require special handling in the spec.

I'd be happy to eliminate the special handling in Array.of and Array.from if there is consensus that it isn't desirable.

I don't need it. (Although es6-shim currently seems to implement the special handling correctly.)

# Andreas Rossberg (11 years ago)

On 11 June 2014 18:44, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

I'd be happy to eliminate the special handling in Array.of and Array.from if there is consensus that it isn't desirable.

I'd be in support of that.

# Rick Waldron (11 years ago)

On Wed, Jun 11, 2014 at 12:44 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

I'd be happy to eliminate the special handling in Array.of and Array.from if there is consensus that it isn't desirable.

I don't see the incentive in removing the special handling from these two cases when there is obvious end-programmer benefit: minifiers can safely alias Array.of and Array.from without needing to use .bind(Array) when declaring that alias.

# C. Scott Ananian (11 years ago)

Saving 12 bytes does not seem like an "obvious end-programmer benefit" to me. It seems like unnecessary and premature optimization.

# Domenic Denicola (11 years ago)

It's also deceptive: it makes you think Array.of and Array.from are functions, when in reality they are definitely methods.

# Tom Van Cutsem (11 years ago)

2014-06-11 18:02 GMT+02:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:

Kind of boarder line. 6.1.7.2 says that the "essential internal methods" are those listed in Table 5 (does not include [[Call]] and [[Constructor]]). Also the definitions of [[Call]] and [[Construct]] in 9.5 each include a note that says these internal methods are only provided when a corresponding internal method exists on the target.

Reading the section as a while, I think it is pretty clear that that a Proxy only has a [[Construct]] internal method if its target also has one.

Ok, the fact that an explicit note is included in 9.5.13 [[Call]] and 9.5.14 [[Construct]] should be sufficient to avoid any doubt.

# Allen Wirfs-Brock (11 years ago)

I may still try to tighten up the language. I usually consider even one person being uncertain about the meaning of some prose in the spec. to be an indication that others will probably also be confused.

# Domenic Denicola (11 years ago)

From: Allen Wirfs-Brock <allen at wirfs-brock.com>

There are a few uses of IsConstructor in some Array methods that deal with subtle backwards compat issues that are a result of extending Array to be subclassable. These are very unique cases and I don't think you should look at them as establishing a pattern that should be followed in other situations.

Can you expand on these a bit more? What would happen if they threw when used on non-constructors?

# Allen Wirfs-Brock (11 years ago)

Here is some ES5 code that would fail if the IsConstructor test in map threw for a non-constructor:

var o = [];
o.constructor = undefined;
o.map(function(){}) instanceof Array
/*
true
*/

We're already walking a compatibility tight-rope to make sure that:

class SubA extends Array {};
( new SubA).map(x=>x) instanceof SubA   //true in ES6, would have been false in ES5

It's hard to say where we need to maintain strict backwards compatibility and where we can diverge to support new functionality but an over-riding ES6 guideline has been to bend backwards to minimize breaking changes.

# Rick Waldron (11 years ago)

On Wed, Jun 11, 2014 at 1:37 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

It's also deceptive: it makes you think Array.of and Array.from are functions, when in reality they are definitely methods.

Yes, you're right.

What about Array subclasses? from and of are "inherited" when Array is subclassed, so these:

Which are are how from and of determine what to construct, will be replaced with a single step, being one of:

  • Let A be the result of calling the [[Construct]] internal method of C with an empty argument list.
  • Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.

But surely this step will also have to be guarded by IsCallable(this), which that means this:

  • If IsConstructor(C) is true, then
    • Let A be the result of calling the [[Construct]] internal method of C with an empty argument list.
  • Else,
    • Let A be ArrayCreate(0).

Becomes:

  • If IsCallable(C) is true, then
    • Let A be the result of calling the [[Construct]] internal method of C with an empty argument list.
  • Else,
    • Let A be ArrayCreate(0).

Or maybe that's not necessary? Is it preferable to just throw when someone writes any of these:

let of = Array.of;
of(1,2,3,4);

let from = Array.from;
from(arrayLikeOrIterable);

let List = {
  of: Array.of
};

List.of(1, 2, 3, 4);

(Note that Array.isArray doesn't cease to work correctly when aliased)

With the IsConstructor or IsCallable guard, these "just work"; without any guard, they'll throw TypeError exceptions: "object is not a function" or "undefined is not a function" (in strict mode). Neither of these errors are very obvious. Of course this can all be fixed with .bind() or a "bind" operator, but it just seems unfortunate to throw out something that's not harming the spec in favor something that might be problematic in end user code.

# Domenic Denicola (11 years ago)

From: Rick Waldron <waldron.rick at gmail.com>

Or maybe that's not necessary? Is it preferable to just throw when someone writes any of these:

I think it is indeed preferable, as would happen when using any other method (this-dependent function) without a this.

(Note that Array.isArray doesn't cease to work correctly when aliased)

Array.isArray is not a method, but a function; it does not change behavior depending on its this value.

With the IsConstructor or IsCallable guard, these "just work"; without any guard, they'll throw TypeError exceptions: "object is not a function" or "undefined is not a function" (in strict mode). Neither of these errors are very obvious.

I disagree that this is not obvious. This is the same error you always get when aliasing a method and trying to use it as a function.

Of course this can all be fixed with .bind() or a "bind" operator, but it just seems unfortunate to throw out something that's not harming the spec in favor something that might be problematic in end user code.

This is a bit of a slippery-slope argument; the end result is that every method should have a default this value that it's "soft-bound" to, in order to coddle the users who might get confused.

# C. Scott Ananian (11 years ago)

On Wed, Jun 11, 2014 at 3:27 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

This is a bit of a slippery-slope argument; the end result is that every method should have a default this value that it's "soft-bound" to, in order to coddle the users who might get confused.

And from my perspective, trying to fix the users' bugs for them is a very dangerous slope indeed. You are producing behavior that doesn't match the user's mental model of how this should work (ie, how it works for all other methods). Some developer is going to get badly confused when a falsy or unusual this doesn't throw a TypeError as they expect (allowing them to identify and debug the problem) but instead silently "succeeds" returning an object of an arbitrary class.

I was walking a newish JS developer through a similar issue last night, when they wanted to know why the standard JS inheritance doesn't work for Error:

var MyError = function(msg) { Error.call(this, msg); };
MyError.prototype = new Error();
MyError.prototype.constructor = MyError;

console.log((new MyError("foo")).message); // prints... undefined ?!

Why doesn't message get initialized? Surprise, it's because in ES5 invoking Error as a function ignores the given this and substitutes a new one, which is a brand-new instance of Error, and returns that. This made my developer friend cry. (This is hacked around in ES6 so that class MyError extends Error is sane.)

We shouldn't make this mistake in ES6. Use the this which the author of the code provided and don't second-guess them, or else they will tear at their hair in confusion.

# Allen Wirfs-Brock (11 years ago)

On Jun 11, 2014, at 11:59 AM, Rick Waldron wrote:

Becomes:

If IsCallable(C) is true, then

that's not an adequate guard, be an object can be callable but not have a [[Construct]] internal method so the next step not be valid

# Rick Waldron (11 years ago)

On Wed, Jun 11, 2014 at 3:27 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

Array.isArray is not a method, but a function; it does not change behavior depending on its this value.

I wasn't arguing that it was or wasn't, but I guess my point wasn't clear, so that's my mistake: most web developers won't know or care about the difference.

This is a bit of a slippery-slope argument; the end result is that every method should have a default this value that it's "soft-bound" to, in order to coddle the users who might get confused.

Fair enough, I give up, it's all yours to behead.

# Rick Waldron (11 years ago)

On Wed, Jun 11, 2014 at 4:04 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

that's not an adequate guard, be an object can be callable but not have a [[Construct]] internal method so the next step not be valid

I wasn't recommending this, I was illustrating that removing IsConstructor benefits no one and hurts the overall programming experience. This one only one layer of the illustration. Either way, I've given up on presenting reasons why the IsConstructor guard was useful. It's too bad that no one who wanted those aliasing options bothered to speak up.

# Jason Orendorff (11 years ago)

On Wed, Jun 11, 2014 at 11:44 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

I don't recall why we provided that functionality. It doesn't seem to me like a pattern we should want to encourage.

I think it was meant for functional programming; but FP is so poorly supported by the rest of the standard library that it's not useful by itself.

Array.of could test 'if the this value is undefined' rather than using IsConstructor(). I like that. I can't put my finger on it, but I suspect people will try to treat Array.of as a standalone function and get the "undefined is not a constructor" error, and be baffled.

Back to the topic, it seems weird to go out of our way to expose @@isRegExp and @@isConcatSpreadable and also go out of our way to hide IsConstructor(). I don't like "does this object conform to this protocol" tests, but they are a fact of life in real JS code.

# Allen Wirfs-Brock (11 years ago)

On Jun 12, 2014, at 5:36 AM, Jason Orendorff wrote:

Array.of could test 'if the this value is undefined' rather than using IsConstructor(). I like that. I can't put my finger on it, but I suspect people will try to treat Array.of as a standalone function and get the "undefined is not a constructor" error, and be baffled.

The problem with the undefined test is that it doesn't work if somebody tries to attach such functions to a namespace object:

let arraybuilder = {of: Array.of, from: array:Array.from};
arraybuilder.of(1,2,3,4);

or consider, at the global level:

var of = Array.of;
of(1,2,3); //works
this.of(1,2,3) //breaks

That's essentially why we have the IsConstructor test. To distinguish between this values that are actual constructors that will be used to create the new collection and non-constructor objects that are just contains for the function.

Back to the topic, it seems weird to go out of our way to expose @@isRegExp and @@isConcatSpreadable and also go out of our way to hide IsConstructor(). I don't like "does this object conform to this protocol" tests, but they are a fact of life in real JS code.

I think the @@is methods and isConstructor are different kinds of beasts. isConstructor wold be a very general predicate that queries a fundamental characteristic of the meta-object protocol. The @@is methods are local to the implementation of a specific abstraction and nobody really needs to know about them unless they are trying to extend that abstraction.

I'm not really opposed to an isConstructor predicate, I'm just pushing back to see if it is something that really needs to be exposed. If we have it, I think we probably should also have a isCallable predicate and I'd hand both of them off of Function. IE:

Function.isCallable(value)
Function.isConstructor(value)  //or maybe it should be Function.isNewable ?
# Domenic Denicola (11 years ago)

I'd be most interested in seeing if we can remove IsConstructor entirely (except for uses where it's just a guard, implementing the semantics of new via IsConstructor -> [[Construct]] or throw).

It seems like there's at least some movement toward removing it from Array.of and Array.from. All that remains is its use to preserve the arrayInstance.constructor = undefined backward-compatibility possibilities. My preference would be to see if we can get away with breaking that use case, and reintroduce it if that turns out not to be web-compatible.

# Allen Wirfs-Brock (11 years ago)

There's a lot of subtle stuff going on with these compatibility cases and addressing them was something we needed to do to get consensus around supporting subclassing of Array. I don't think we want to mess them up simply to avoid spec. level use of IsConstructor. I really don't expect anybody to seriously try to self-host the compatibility aspects of those methods and there is little point in trying to do a ES5 level shim that includes that functionality because other aspects of of subclassing just won't work in ES5.

# Erik Arvidsson (11 years ago)

On Thu Jun 12 2014 at 10:59:57 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

The problem with the undefined test is that it doesn't work if somebody tries to attach such functions to a namespace object:

It should not work. These are methods that depends on this.

var ns = {create: document.createElement};
ns.create('div');  // throws wrong receiver
let arraybuilder = {of: Array.of, from: array:Array.from};
arraybuilder.of(1,2,3,4);

or consider, at the global level:

var of = Array.of;
of(1,2,3); //works
this.of(1,2,3) //breaks

Why can't we "blindly" call this[[Construct]]? It will throw for all of the above cases which is pretty much what one would expect.

# André Bargull (11 years ago)

I'd be most interested in seeing if we can remove IsConstructor entirely (except for uses where it's just a guard, implementing the semantics of new via IsConstructor -> [[Construct]] or throw).

It seems like there's at least some movement toward removing it from Array.of and Array.from. All that remains is its use to preserve the arrayInstance.constructor = undefined backward-compatibility possibilities. My preference would be to see if we can get away with breaking that use case, and reintroduce it if that turns out not to be web-compatible.

The [[Realm]] check in Array.prototype.* is even more annoying than the IsConstructor guard, but unfortunately required for web-compatibility per [1]. :-(

[1] esdiscuss.org/topic/array

# Allen Wirfs-Brock (11 years ago)

On Jun 12, 2014, at 8:30 AM, Erik Arvidsson wrote:

Why can't we "blindly" call this[[Construct]]? It will throw for all of the above cases which is pretty much what one would expect.

I already said I'd be fine with that. Personally I think the practice of high-jacking methods and turning them into naked functions is something we should discourage.

It when we start trying to give a function both this-dependent and this-independent functional behavior that we get into the weeds.

# Erik Arvidsson (11 years ago)

On Thu Jun 12 2014 at 11:38:22 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

It when we start trying to give a function both this-dependent and this-independent functional behavior that we get into the weeds.

Yes. Lets not do that :-)

# Allen Wirfs-Brock (11 years ago)

On Jun 12, 2014, at 8:33 AM, André Bargull wrote:

The [[Realm]] check in Array.prototype.* is even more annoying than the IsConstructor guard, but unfortunately required for web-compatibility per 1. :-(

Yes! Please don't rock the boat with regard to this stuff. It's all there to address real compatibility issues.

# Jason Orendorff (11 years ago)

On Thu, Jun 12, 2014 at 10:06 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

I'd be most interested in seeing if we can remove IsConstructor entirely (except for uses where it's just a guard, implementing the semantics of new via IsConstructor -> [[Construct]] or throw).

It seems like there's at least some movement toward removing it from Array.of and Array.from. All that remains is its use to preserve the arrayInstance.constructor = undefined backward-compatibility possibilities.

Proxies also observe it.

I really wish we just had an @@new for this, such that

new C(...args)

is just shorthand for

C[Symbol.new](...args)

and the construct trap could be removed from proxies altogether. :-|

# Jason Orendorff (11 years ago)

I forgot to add: then the rare code that really wants to ask "is x a constructor?" can say

if (x[Symbol.new] !== undefined) ...

like Array.from does for @@iterator.

# C. Scott Ananian (11 years ago)

+1.

Function.@@new could be added to ES7, but the ugly construct trap will be with us forever.

# Allen Wirfs-Brock (11 years ago)

At TC39 meetings (and probably on some es-discuss threads) we've talked about eliminating [[Construct]] and simply inlining its ordinary definition (more or less: C.apply(C[Symbol.create](), args)). We didn't reach consensus to do so. I believe that some of the concern was simply not knowing whether all of the DOM [[Construct]] semantics could be successfully replaced replaced using only @@create methods and constructor bodies.

Another possible concern is an exotic [[Construct]] can't be invoked any way other than using the new operator. A @@create (or a hypothetical @@new) can be copied around and invoked from arbitrary places. This raises concerns about whether their might be security exploits that could be built upon them.

It's not obvious to me why we would need @@new in addition to @@create (which is pretty much equivalent to saying it's not clear to me why we need [[Construct]]). For the ordinary case, @@new would just be another level of method lookup and invocation that would be required on each new. While we expect implementations to (eventually) optimize all of this, we still tried to minimize the amount of boiler plate work required for each new.

# Boris Zbarsky (11 years ago)

On 6/12/14, 3:21 PM, Allen Wirfs-Brock wrote:

simply not knowing whether all of the DOM [[Construct]] semantics could be successfully replaced replaced using only @@create methods and constructor bodies.

WebIDL currently doesn't use a custom [[Construct]] at all. It uses a custom [[Call]] on DOM constructors.

Chances are, we want to move from that to using @@create or whatever is needed to allow subclassing. In either case, I don't think we'll be doing custom [[Construct]] in the DOM.

# Allen Wirfs-Brock (11 years ago)

On Jun 12, 2014, at 12:25 PM, Boris Zbarsky wrote:

WebIDL currently doesn't use a custom [[Construct]] at all. It uses a custom [[Call]] on DOM constructors.

Is the custom [[Call]] only use to implement WebIDL overload/argument processing semantics? Or do you perform object allocations within the [[Call]. Have you looked at how such constructors would behave when a subclass constructor does a super call to them?

Chances are, we want to move from that to using @@create or whatever is needed to allow subclassing. In either case, I don't think we'll be doing custom [[Construct]] in the DOM.

When we talked about this in TC29 I don't think anybody identified any specific cases where they knew it would be an issue. The hesitation was more about uncertainty concern the unknown.

# Boris Zbarsky (11 years ago)

On 6/12/14, 3:38 PM, Allen Wirfs-Brock wrote:

Is the custom [[Call]] only use to implement WebIDL overload/argument processing semantics? Or do you perform object allocations within the [[Call].

Right now the [[Call]] allocates a new object of the right sort.

Have you looked at how such constructors would behave when a subclass constructor does a super call to them?

We need to figure out how to support subclassing, yes. The current setup does not.

# C. Scott Ananian (11 years ago)

On Thu, Jun 12, 2014 at 3:21 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

It's not obvious to me why we would need @@new in addition to @@create (which is pretty much equivalent to saying it's not clear to me why we need [[Construct]]). For the ordinary case, @@new would just be another level of method lookup and invocation that would be required on each new. While we expect implementations to (eventually) optimize all of this, we still tried to minimize the amount of boiler plate work required for each new.

From my perspective, it's about simplifying the language. I like the fact that 'new C' is "just" sugar for an ordinary method invocation on C. It would simplify the presentation of the spec as well: various places that currently state special behavior for "new XYZ" forms could instead describe the ordinary method XYZ.@@new. And as Jason points out, this conceptual simplification of the language translates into concrete API simplifications for reflective operations like Proxies.

I don't think there are any special security issues involved, since I can already do: let x = Reflect.construct.bind(Reflect, C) and pass that around.

(fwiw, Crockford's "Simplified JavaScript" in javascript.crockford.com/tdop/tdop.html and my own "TurtleScript" subset of JavaScript in cscott/TurtleScript both did away with the new operator. TurtleScript replaced it with Function#New.)

# Tom Van Cutsem (11 years ago)

Interesting. It'd be nice if we could further simplify the Proxy/Reflect API. Given the local nature of these changes, we might still include it in ES6 if we achieve quick consensus.

As Allen mentioned, this came up a number of times in TC39 meetings and I believe the fear for exotic objects that require control over [[Construct]] was the biggest show-stopper. If more knowledgeable people like Boris can vouch for the fact that this isn't the case, that would remove the biggest roadblock.

Scott is right: Reflect.construct essentially directly calls [[Construct]], so the Reflect API already provides the power to construct objects without the new operator. There are no security concerns that I can spot. Cc'ing Mark.

One important detail:

Jason proposes:

new C(...args) => C[Symbol.new](...args)

Allen proposes:

new C(...args) =>  C.apply(C[Symbol.create](), args)

I prefer Jason's transformation for the following reason: imagine a proxy that wants to implement e.g. a flyweight pattern (i.e. it wants to pool or cache objects, rather than constructing a fresh object for each call to |new|). This is trivial to do with Jason's transformation, because the proxy is in control of both object allocation and initialization. With Allen's transformation, we need to jump through some hoops if the cached object returned by Symbol.create depends on args (which is common for flyweights), as they only get passed in after Symbol.create returns.

We can debate whether C[Symbol.new] should be called with args collected in an array or spliced. I have no preference.

# Andreas Rossberg (11 years ago)

I strongly prefer Allen's version. Frankly, I'm already quite concerned about the proliferation of extension hooks for fringe use cases in ES6. Allen's version is a net win in language complexity, while the other is not.

# Boris Zbarsky (11 years ago)

On 6/13/14, 6:33 AM, Tom Van Cutsem wrote:

As Allen mentioned, this came up a number of times in TC39 meetings and I believe the fear for exotic objects that require control over [[Construct]] was the biggest show-stopper. If more knowledgeable people like Boris can vouch for the fact that this isn't the case, that would remove the biggest roadblock.

The basic question is how that affects what WebIDL needs here is how it should play with subclassing.

If we don't care about subclassing WebIDL objects, there is no issue: WebIDL can just override [[Call]] as it does right now and we move on with life. Note that some implementations at the moment override [[Construct]], not [[Call]] and just throw from [[Call]], so as to preclude web pages from relying on the current spec's [[Call]] behavior, since that would presumably interact badly with trying to introduce subclassing.

If we do care about subclassing, then presumably the "construct the right sort of object" and "initialize it" actions need to be somewhat decoupled for these objects. It seems to me that both Allen's current spec and Jason's proposal accomplish that, right?

In Jason's proposal it would look like this, if I understand it right:

   subclass[Symbol.new] = function(...args) {
     // Modify args as desired.
     var obj = superclass[Symbol.new](...args);
     // Modify obj as desired.
     return obj;
   }

which is pretty similar to how this would be done without classes:

   function subclass(...args) {
     // Assume that we're being invoked via new.
     // Modify args as desired.
     var obj = new superclass(...args);
     // Modify obj as desired.
     return obj;
   }

except there is nothing that needs to be done if you don't want to munge args or obj, since not defining Symbol.new on the subclass will simply find it on the superclass.

In Allen's spec, it would look like this:

   "subclass constructor": function(...args) {
     // "this" is already set to the right object that got created via
     // looking up Symbol.create and calling it.
     // Modify args as desired.
     superclass.apply(this, ...args);
     // Modify "this" as desired.
   }

Is my understanding of the Allen and Jason's proposals correct? I'd like to avoid making any claims of how these interact with WebIDL until I'm sure I'm not missing something important.

# C. Scott Ananian (11 years ago)

On Fri, Jun 13, 2014 at 6:33 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

Jason proposes:

new C(...args) => C[Symbol.new](...args)

Allen proposes:

new C(...args) =>  C.apply(C[Symbol.create](), args)

Consider also the way the spec could read. For example, for the 'Error' object, instead of having "19.5.1.1 Error ( message )" and "19.5.1.2 new Error ( ...argumentsList )", in Jason's formulation section 19.5.1.2 would just be "Error[ @@new ]]", aka an ordinary method definition. "If Error is implemented as an ECMAScript function object, its inherited implementation of @@new will perform the above steps."

# Allen Wirfs-Brock (11 years ago)

The existence or not of @@new doesn't make a difference in that regard. The only reason we current need the "new Foo(...)" specifications is because [[Construct]] exists and built-ins are not necessarily ECMAScript functions so we have to say what their [[Construct]] behavior is.

If [[Construct]] was eliminated that reason would go away and there would be no need for the "new Foo(...) built-in specifications.

The only difference between inlining ordinary [[Construct]] into the 'new' operator semantics and defining the 'new' operator as invoking @@new is the amount of freedom anES programmer would have in defining how a @@create method relates to its companion constructor function. Without @@new there is a fixed protocol for how @@create and the constructor function are invoked relative to each other. With @@new a different protocol (including ones that completely ignores @@create and the constructor function) could be implemented in ES code.

It isn't clear if there are really any use cases that could be supported by @@new that couldn't also be support by carefully crafting a @@create and companion constructor function.

Another concern I have with @@new, it that it exposes two extension points, @@new and @@create, on very constructor. I'm afraid that it wouldn't be very clear to most ES programmers when you should over-ride one or the other.

Finally, let's say that for ES6 we eliminate [[Construct]] without adding @@new. If after some experience we find that @@new is really needed we can easily add it in a backwards compatible manner.

# Boris Zbarsky (11 years ago)

On 6/13/14, 1:21 PM, Allen Wirfs-Brock wrote:

It isn't clear if there are really any use cases that could be supported by @@new that couldn't also be support by carefully crafting a @@create and companion constructor function.

I agree. I was assuming in my mail trying to make sure I understand the implications of @@new and @@create that @@new would be a replacement for @@create, not something alongside it. I'm not sure here are particularly good reasons to have both, unless I'm really missing something.

# Allen Wirfs-Brock (11 years ago)

On Jun 13, 2014, at 7:51 AM, Boris Zbarsky wrote:

The basic question is how that affects what WebIDL needs here is how it should play with subclassing.

If we don't care about subclassing WebIDL objects, there is no issue: WebIDL can just override [[Call]] as it does right now and we move on with life. Note that some implementations at the moment override [[Construct]], not [[Call]] and just throw from [[Call]], so as to preclude web pages from relying on the current spec's [[Call]] behavior, since that would presumably interact badly with trying to introduce subclassing.

This "we" definitely cares about subclassing WebIDL objects and the ES6 object creation protocol was design with that in mind.

If we do care about subclassing, then presumably the "construct the right sort of object" and "initialize it" actions need to be somewhat decoupled for these objects. It seems to me that both Allen's current spec and Jason's proposal accomplish that, right?

Right, Jason's proposal introduces an additional degree of variability that my proposal (assuming that [[Construct goes away) doesn't directly provide. It kind of comes down to whether that extra flexibility is actually needed.

In Jason's proposal it would look like this, if I understand it right:

 subclass[Symbol.new] = function(...args) {
   // Modify args as desired.
   var obj = superclass[Symbol.new](...args);
   // Modify obj as desired.
   return obj;
 }

I don't think so. Unless, I misunderstand I don't think Jason is proposing eliminating @@create. So, in most cases including, I believe, the WebIDL use cases a constructor would simply use the default @@new that would be inherited from Function.prototype. It's definition would be something like:

Function.prototype[Symbol.new] = function (...args) {
      let newObj = this[Symbol.create]();
      if (! IsObject(newObj) {
           //default handling for ill-behaved or missing @@create
      }
      let ctorResult = this(...args);
     if (IsObject(ctorResult) return ctorResult;  //ES<6 compatibility 
     else return newObj;
}

Note that this preserves the separation between object allocation and object initialization we need to make subclassing work correctly.

which is pretty similar to how this would be done without classes:

 function subclass(...args) {
   // Assume that we're being invoked via new.
   // Modify args as desired.
   var obj = new superclass(...args);
   // Modify obj as desired.
   return obj;
 }

Except that the above doesn't give you true subclassing. The object you return is an instance of the superclass and not of the subclass. The key thing that @@create is doing is separating determining the physical characteristics of an object (making it exotic, branding it, using a custom C struct as its representation, etc.) from logically initializing it at an appropriate place within some class hierarchy

Is my understanding of the Allen and Jason's proposals correct? I'd like to avoid making any claims of how these interact with WebIDL until I'm sure I'm not missing something important.

I think you are missing the key element @@create plays in both proposals. The only real difference, I think, is whether you have to live with the default @@create/constructor protocol or whether you are allowed to tweak with that default.

# Boris Zbarsky (11 years ago)

On 6/13/14, 1:51 PM, Allen Wirfs-Brock wrote:

Unless, I misunderstand I don't think Jason is proposing eliminating @@create.

I had assumed he was... I'm not sure I see the point if we're not doing that.

The object you return is an instance of the superclass and not of the subclass.

Ah, right.

So just to make sure, now that I think about it: in the @@create setup, where is the prototype of the object set? What is the prototype of the thing @@create returns?

Or put another way, is there really a difference, conceptually, between doing the "create an instance of the superclass and then change the prototype" thing and what @@create gives us?

The key thing that @@create is doing is separating determining the physical characteristics of an object (making it exotic, branding it, using a custom C struct as its representation, etc.) from logically initializing it at an appropriate place within some class hierarchy

Looking at what's involved in object allocation in at least SpiderMonkey, the following pieces of data are required to be known at object creation time:

  1. The Realm to use.
  2. Some metainformation about the exotic-or-not nature of the object, its internal layout, etc.

These are effectively not mutable in SpiderMonkey.

In addition to those, it's good to have the prototype available at object-creation time as well, since later dynamic prototype mutation, while possible, leads to deoptimization in the JIT. This may be a SpiderMonkey-specific issue, of course.

# Allen Wirfs-Brock (11 years ago)

On Jun 13, 2014, at 11:02 AM, Boris Zbarsky wrote:

So just to make sure, now that I think about it: in the @@create setup, where is the prototype of the object set? What is the prototype of the thing @@create returns?

yes.

The default @@create is approximately:

Function.prototype[Symbol.create] = function(I) {
    return Object.create(this.prototype)
}

Or put another way, is there really a difference, conceptually, between doing the "create an instance of the superclass and then change the prototype" thing and what @@create gives us?

No, except that changing the [[Prototype]] after the fact require a mutable [[Prototype]] and I can imaging that some object representations might not want to allow that.

  1. The Realm to use.

normally that would be the same Realm as that of the @@@create method. But an @@create method could decide otherwise.

  1. Some metainformation about the exotic-or-not nature of the object, its internal layout, etc.

These are effectively not mutable in SpiderMonkey.

Right, that is essentially the primary motivation for @@create. There are some low level characteristics of some objects that must be fixed when the storage of the object is allocated.

In addition to those, it's good to have the prototype available at object-creation time as well, since later dynamic prototype mutation, while possible, leads to deoptimization in the JIT. This may be a SpiderMonkey-specific issue, of course.

+1

# Jussi Kalliokoski (11 years ago)

On Fri, Jun 13, 2014 at 8:21 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

If [[Construct]] was eliminated that reason would go away and there would be no need for the "new Foo(...) built-in specifications.

Yes, please. The whole concept of [[Construct]] is just very confusing in my opinion, and makes especially little sense from the userland perspective. For a userland function, it's practically impossible to statically analyze whether that function is a constructor or not, consider these for example: gist.github.com/jussi-kalliokoski/7f86ff181a01c671d047 . Which of them are constructors and which are not? And how can you tell? IsConstructor would have to say true for every JS-defined function out there to give even close to usable results, and say false only for host-defined functions. If we were to then use IsConstructor anywhere, it would deepen the gap between host objects and native objects even further, which I don't think anyone wants. In JS itself (as it currently is), depending on how you think about it, there either aren't constructors at all or every function is a constructor.

Finally, let's say that for ES6 we eliminate [[Construct]] without adding @@new. If after some experience we find that @@new is really needed we can easily add it in a backwards compatible manner.

To me this sounds like a very reasonable idea, +100, but of course I'm not too well aware of how widely it's used.

One question about @@create though... What would this do:

function Foo () {}

Foo.prototype[Symbol.create] = null;

// ???
// Maybe error out, like currently host objects without [[Construct]]:
// TypeError: Foo is not a constructor.
new Foo();
# André Bargull (11 years ago)

One question about @@create though... What would this do:

function Foo () {}

Foo.prototype[Symbol.create] = null;

// ???
// Maybe error out, like currently host objects without [[Construct]]:
// TypeError: Foo is not a constructor.
new Foo();
function Foo(){}
Object.defineProperty(Foo, Symbol.create, {value: null});
new Foo();

Results in uncaught exception: TypeError: "Symbol(Symbol.create)" is not a function.

Whereas this version creates a default object with [[Prototype]] set to Bar.prototype:

function Bar(){}
Object.defineProperty(Bar, Symbol.create, {value: void 0});
new Bar();

The exact behaviour is specified in CreateFromConstructor(F) and Construct(F, argumentsList)

people.mozilla.org/~jorendorff/es6-draft.html#sec-createfromconstructor

people.mozilla.org/~jorendorff/es6-draft.html#sec

# Allen Wirfs-Brock (11 years ago)

On Jun 13, 2014, at 12:07 PM, Jussi Kalliokoski wrote:

function Foo () {}

Foo.prototype[Symbol.create] = null;

@@create methods are normally defined as methods of the constructor function rather than as an instance method on the prototype. So the above should be:

Foo[Symbol.create] = null;
// ???
// Maybe error out, like currently host objects without [[Construct]]:
// TypeError: Foo is not a constructor.
new Foo();

as currently specified [1] (and after I fix a bug I just noticed) it will fall back to doing the equivalent of Object.create().

I did that to maximize backwards compatibility for this specific situation:

function Foo() {};
Foo.__proto__ = null; //in ES6 default @@create inherited from Function.prototype becomes unavailable
new Foo;    //but in ES1-5 this still does the equivalent of Object.create();

If we go down the route of eliminating [[Construct]] and are willing to break backwards compat. for this case, then we could make 'new' throw if a constructor doesn't have a callable @@create. That would provide a good way to indicate that a function is not a constructor (which is current determine based upon it having a [[Construct]] internal method.

# Erik Arvidsson (11 years ago)

On Fri Jun 13 2014 at 3:41:02 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

I did that to maximize backwards compatibility for this specific situation:

function Foo() {};
Foo.__proto__ = null; //in ES6 default @@create inherited from
Function.prototype becomes unavailable
new Foo;    //but in ES1-5 this still does the equivalent of
Object.create();

Hold on. We covered this in one of the face to face meetings and I was of the impression it was decided that we should not have a fallback but instead throw.

rwaldron/tc39-notes/blob/48c5d285bf8bf0c4e6e8bb0c02a7c840c01cd2ff/es6/2013-01/jan-29.md#48

# Allen Wirfs-Brock (11 years ago)

My impression was that the discussion on that pooint was inconclusive. This margin note has been in the ES6 draft for a couple of years:

"At Jan 29, 2012 TC39 serveral peopled suggest that this fall back was unnecessary complexity and that it should this throw. However, that means that an ECMAScript function whose __proto__ is set to null will throw if newed. I’m not sure that is desirable. It’s a breaking change for the reality web."

I don't think we had consensus about whether we are willing to accept such a breaking change. However, the eliminating [[Construct]] path would probably push me over the edge into accepting that.

# Boris Zbarsky (11 years ago)

On 6/13/14, 2:20 PM, Allen Wirfs-Brock wrote:

The default @@create is approximately:

Function.prototype[Symbol.create] = function(I) {
     return Object.create(this.prototype)
}

Ah, I see. So the point is in normal use @@create is passed the right prototype to start with implicitly, via "this".

Doing that for @@create in the DOM might not be very backwards compatible, but we could probably do something like use the canonical prototype if "this" is the canonical constructor and else do this.prototype.

The main remaining issue with @@create is making all the DOM specs handle a created but not initialized object or something... That's something I'd been hoping to avoid, since it requires wholesale changes to a bunch of specifications, not all of which are even actively edited. :( I suppose we could make things subclassable on an opt-on basis or something to work around that.

# Tom Van Cutsem (11 years ago)

2014-06-13 19:21 GMT+02:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:

Anotherconcern I have with @@new, it that it exposes two extension points, @@new and @@create, on very constructor. I'm afraid that it wouldn't be very clear to most ES programmers when you should over-ride one or the other.

Smalltalk has both extension points (#new and #initialize). I think JS programmers could handle this just as well.

Finally, let's say that for ES6 we eliminate [[Construct]] without adding @@new. If after some experience we find that @@new is really needed we can easily add it in a backwards compatible manner.

By that time, the community will have developed its own set of work-arounds, as it always does.

In any case, I believe you're probably right that @@create + constructor invocation is sufficient for most use cases. The crucial step is that if the constructor returns an object, this is the object returned by |new C(...args)|. This way, the constructor can always return a cached object and ignore whatever dummy object was created by @@create. Correct?

# Allen Wirfs-Brock (11 years ago)

On Jun 15, 2014, at 11:45 PM, Tom Van Cutsem wrote:

Smalltalk has both extension points (#new and #initialize). I think JS programmers could handle this just as well.

and #basicNew. It seeded to me that many Smalltalk programmer were unclear about when to over-ride #new vs when to over-ride #basicNew.

In any case, I believe you're probably right that @@create + constructor invocation is sufficient for most use cases. The crucial step is that if the constructor returns an object, this is the object returned by |new C(...args)|. This way, the constructor can always return a cached object and ignore whatever dummy object was created by @@create. Correct?

right.

# Jason Orendorff (11 years ago)

On Fri, Jun 13, 2014 at 5:33 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

Interesting. It'd be nice if we could further simplify the Proxy/Reflect API. Given the local nature of these changes, we might still include it in ES6 if we achieve quick consensus.

I'm mostly interested in getting rid of [[Construct]]. Three cheers for that!

I like @@new better than @@create for three reasons:

  1. I think it wins on common sense over @@create. "What's this Symbol.new thing in this class?" "Oh, whenever you see new X(), that's shorthand for the @@new method." Pretty straightforward.

  2. I guess the purpose of @@create, to my mind, was to provide some invariants: in particular, an object, once visible to script, is never observed to "become" an Array object or "stop being" an Array object. Its initialization is hidden in that method. But @@new does that just as well or better, it seems to me. "Better" because, well, read on.

  3. I'm uncomfortable with how all the constructor functions in ES5 have been rewritten in ES6 to have strange this-value checks and "has this value already been initialized? if so, throw" checks. Rarely-taken branches are bad for us --- it's a lot of tricky code implementers will have to write, test, and optimize, for something that should never happen. Providing @@new methods corresponding to the ES5 [[Construct]] methods seems more to the point, clearer to specify, and easier to implement.

# Allen Wirfs-Brock (11 years ago)

Based upon the above, I don't think I completely misunderstood the semantics you have in mind for @@new. It sounds like you are saying that a @@new method would provide all the legacy "as as a constructor behavior" and that the constructor body would provide the legacy "called as a function" behavior. But presumably to enable: function f(x) {this.x = x}

to do the expected thing the default @@new behavior would have to call into the regular body of the function. In that case, aren't you right back with the same problems of trying to discriminate in a function like f whether it is being called independently or called to initialize something created by @@new?

Perhaps you could fill out what you actually mean by @@new and how it would work for regular functions, class declarations, and subclasses of both regular classes and the legacy built-ins.

# Jason Orendorff (11 years ago)

On Mon, Jun 16, 2014 at 5:39 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

Perhaps you could fill out what you actually mean by @@new and how it would work for regular functions, class declarations, and subclasses of both regular classes and the legacy built-ins.

Yes, that's a very good idea. I'll do so.