Accesssing ES6 class constructor function

# James Treworgy (7 years ago)

I am brand new to this list, I find myself here because of a confounding issue related to ES6 classes vs. traditional constructors. Forgive me if this is something that's been hashed out in times past. I looked around for discussion online and couldn't find anything more than the observation that the spec prohibits invoking it - not really any discussion. Probably a failing of google more than anything else, so if there's some discussion that I should read to catch up please point me there.

Here's my issue. The ES6 spec prohibits invoking class constructors without "new". This makes such functions a special case, e.g.

class Test() {}

// typeof Test === 'function'  // yep
// Test.prototype.constructor === Test // yep

// Test() => nope ... TypeError: Class constructor Test cannot be invoked

without 'new'
// Test.call() ... nope
// Test.apply() ... nope

This has some interesting consequences. It means testing something for typeof "function" no longer guarantees it can be invoked without error. Also "function.toString()" can now return something that isn't actually a legal function definiton (since it returns the whole class as text). There seems to be no method, through various javascript reflection/invocation techniques or otherwise, to invoke a class constructor except by creating an instance of the class.

For tool-builders the consequences of this are significant. It's no longer possible to create something that can extend/wrap/act on a prototype by intercepting it's construction process, as it was before with plain ES5 constructors. So classes are fundamentally different than prototype contructors in how we can use them, far more than syntactic sugar. This has come into play lately for me, as an DI container we use that does exactly this doesn't work with ES6 classes (and as far as I can tell, there's no way to make it work, other than having devs no longer use class syntax).

This seems a strange design decision. Even conventional OO languages like C# have the capability to reflect on classes and access the constructor directly as a function. It seems to fly in the face of the basic openness/dyanamic nature of JavaScript, and more signficantly, creates a kind of backward incompatibility since a function is no longer just a function.

I'm wondering whether I'm missing some mechanism for legally accessing a class constructor as a function (other than parsing the output of toString() and eval!) -- and generally thoughts on this aspect of the ES6 specification.

Thank you!

# Tiddo Langerak (7 years ago)

I don't think it is possible to call the constructor as a function. However, in JavaScript a class is just an object (unlike C# or Java, where it's meta data), which means that you can pass it around and dynamically extend it. E.g. you can intercept class creation like this:

function enhanceClass(c) {
     return class EnhancedClass extends c {
         constructor() {
             //do your magic here
         }
     }
}

class MyClass {}

const EnhancedMyClass = enhanceClass(MyClass);
new EnhancedMyClass();

As for toString & not being able to call a function directly: toString is already overwriteable, and in ES5 code it wasn't uncommon to throw from a constructor if it wasn't called as one (for good reasons). ES6 classes just do that for you.

# Oriol _ (7 years ago)

It means testing something for typeof "function" no longer guarantees it can be invoked without error.

This has never been a guarantee. Consider this code

function Test() {
  throw new TypeError();
}
typeof Test === 'function';  // true
Test();  // TypeError

About the stringifications problems, I don't understand what you are attempting to do. Usually you shouldn't need to stringify a function. Probably there is a better approach.

There seems to be no method [...] to invoke a class constructor except by creating an instance of the class.

Well, the point of constructors is creating instances. If you don't want the instance you can just discard it, but then why call the constructor?

So classes are fundamentally different than prototype contructors in how we can use them, far more than syntactic sugar

Not that different. You can usually translate one syntax to the other, but you may need ES6 features like new.target, super or setPrototypeOf.

# James Treworgy (7 years ago)

@Oriol

This has never been a guarantee

I mean a guarantee that I can simply invoke a function. What the function actually does is of course beyond my control.

About the stringifications problems

I'm not really concerned about this, but rather just noting it out as something that is fundamentally different -- we now have something of type "function" where Function.protoype.toString() doesn't evaluate to legal javascript function declaration. But this point is a distraction from the real concern, yeah -- nothing was ever guaranteed ehre

@Tiddo

(slaps forehead) this might - actually - be enough. I need to think about it a bit. The issues of concern are: ability to name the resulting class dynamically (which I remember some quasi-hack involving using dynamic properties of an object that it's possible), and having a return value from "super" (is that possible? to be compatible with ES5 prototypes that return something from the constructor);

But this does kinda get to the major issue... I think my head is so far in how we do things with prototypes that it didn't occur to me to just use an ES6 language feature. I think the biggest difficulty would be creating an implementation that works with native ES6 constructs as well as ES5 or transpiled classes, but seems like it should be possible.

# T.J. Crowder (7 years ago)

On Thu, Jan 5, 2017 at 5:31 PM, James Treworgy <jamietre at gmail.com> wrote:

I can't address your questions about "why" (I wasn't plugged into the discussions around it), but addressing this:

This has come into play lately for me, as an DI container we use that does exactly this doesn't work with ES6 classes (and as far as I can tell, there's no way to make it work, other than having devs no longer use class syntax).

Can you clarify what prevents it from being made to work? I'm probably missing the point you're making there. For instance, this does some brain-dead DI (injecting an argument in the constructor) by dynamically extending the class:

// The class we'll do DI on
class Original {
    constructor($foo) {
        this.foo = $foo;
    }
    run(num) {
        const result = this.foo.fooMethod(num);
        console.log(`num is ${num}, result is ${result}`);
    }
}

// Brain-dead di function
const di = (cls, Foo) => {
    const o = {
        [cls.name]: class extends cls {
            constructor(...args) {
                super(new Foo(), ...args);
            }
        }
    };
    return o[cls.name];
};

// Ues a class that's been DI'd
const use = Original => {
    new Original().run(42);
};

// Use it in dev
use(di(Original, class Foo {
    fooMethod(num) {
        return num * 2;
    }
}));

// Use it in production
use(di(Original, class Foo {
    fooMethod(num) {
        return num / 2;
    }
}));

That outputs

num is 42, result is 84

num is 42, result is 21

...because of the different injected Foos. (This is obviously a simplistic example.)

Separately, there are some tools you can use, such as Reflect.construct, but granted that does create an instance. For instance, if for some reason you wanted to extend a class without using class:

class A {
    amethod() {
        console.log("amethod");
    }
}

function B() {
    const t = Reflect.construct(A, [], B);
    return t;
}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

B.prototype.bmethod = function() {
    console.log("bmethod");
};

const b = new B();
b.amethod();                 // "amethod"
b.bmethod();                 // "bmethod"
console.log(b instanceof A); // true
console.log(b instanceof B); // true

Of course, that cheats a bit with that return t;. :-)

There are probably some tools that should be added to the list. For instance, there's this proposal for Reflect.isCallable and Reflect.isConstructor). And my bmethod above isn't really a method, so it wouldn't be able to use super; in theory one could argue for a Reflect.makeMethod (but use cases are limited, given class syntax). New tools can be added if persuasive use cases come up (and people step forward to define them and get a champion on board).

But circling back, I could be well wide of the mark above. If you can give us more specifics about use cases that aren't supported, we can probably do better helping with them.

-- T.J.

# T.J. Crowder (7 years ago)

Apologies, my email was apparently slow and I didn't see your reply to Oriol.

On Thu, Jan 5, 2017 at 6:28 PM, James Treworgy <jamietre at gmail.com> wrote:

(slaps forehead) this might - actually - be enough. I need to think about

it a bit. The issues of concern are: ability to name the resulting class dynamically (which I remember some quasi-hack involving using dynamic properties of an object that it's possible)

Yup. :-) Coincidentally, I demonstrated it in my previous note, see the di function where it uses cls.name as a computed property.

, and having a return value from "super" (is that possible? to be compatible with ES5 prototypes that return something from the constructor);

...and my Reflect.construct example may (or may not!) be relevant there.

Best,

-- T.J.

# James Treworgy (7 years ago)

Can you clarify what prevents it from being made to work?

The fundamental feature difference (continuing to think about this!) is that with ES5 constructors, I can create an instance of something from an abitrary constructor provided to me, and inject properties that will be available to it at contruction time. The basic operation of the container might be like this

function createInstance(Cotr, args /* array */) {
    function F() {
       // this is dynamic in reality but simple example of injecting
something...
       this.logger = new Logger();

       var instance = Cotr.apply(this, args)
       return instance; // in case Cotr returns something
    }
    F.prototype = Cotr.prototype;
    return new F();
}

So the Cotr can refer to "this.logger" in the constructor. I don't think there's a way to do this with dynamic class inheritance since you always have to call super() before you can assign any properties in a constructor.

This isn't a dealkiller for the tool overall - I can constructor injection instead:

class Thing
    static _inject = [Logger]
    constructor(deps, ...args) {
        // deps = { logger: Logger, dep2: Dep2, ... }
        Object.assign(this, deps) // or however you want to make them
available
    }
}

This will work fine with the dynamic subclass pattern. it just was nice to have everything done by the framework and get rid of boilerplate, but this also has benefits of classes working without the DI container. :) I'm bringing this into an ES6 project for the first time so I can live with a different pattern.

# Don Griffin (7 years ago)

I too was surprised and disappointed with this restriction on constructors.

The enforcement seems pointless to me, because there are valid patterns such as DI or multiple-inheritance where the JS run-time cannot know how a framework is trying to accomplish the construction sequence.

This is really counter to the general philosophy of JS (imho) and more akin to static languages like Java or C#... over there meta programming (MP) happens in very different ways, but JS had previously made this task simple... sadly this kind of thing starts to make MP harder at every step.

I don't hold much hope that this will be relaxed but it should be. :(

Best, Don

Don Griffin Director of Engineering Sencha, Inc. www.sencha.com

# Logan Smyth (7 years ago)

The enforcement seems pointless to me

So we're all on the same page, this restriction exists because it means class syntax can extend builtin types like Map, Set, Array and Error and such, which cannot be extended (with correct functionality anyway) in standard ES5. By delaying initialization of this, it allows the constructor to drill all the way to the base class, which gets to instantiate the correct type of object. To have this be initialized up front, there would need to be some other mechanism in place to instantiate the correct type of base object.

I feel your pain though, I ran into similar issues when updating our codebase to ES6 classes, though I was able to work around it for our specific usecase by having a base class handle hooking into things.

# Boris Zbarsky (7 years ago)

On 1/5/17 2:21 PM, James Treworgy wrote:

function createInstance(Cotr, args /* array */) { function F() { // this is dynamic in reality but simple example of injecting something... this.logger = new Logger();

   var instance = Cotr.apply(this, args)

So the thing is...

This works for script-defined constructors. But it never worked right for constructors of built-ins.

   return instance; // in case Cotr returns something

At which point you've lost your logger, right?

Specifically, in the ES5 world, if the Cotr that's passed in there is Array or Date, say, then instance != this and you don't get the logger thing.

The behavior of ES6 constructors is meant to be able to explain the construction of such built-ins (and especially the new ones like Map, as well as the various DOM built-ins in browsers) and to allow subclassing such built-ins usefully.

So the Cotr can refer to "this.logger" in the constructor. I don't think there's a way to do this with dynamic class inheritance since you always have to call super() before you can assign any properties in a constructor.

That's unavoidable if you want to allow Cotr to control the actual object allocation, which was one of the design constraints here to enable built-ins to work the same way as non-built-ins.

# Don Griffin (7 years ago)

So we're all on the same page, this restriction exists because it means class syntax can extend builtin types like Map, Set, ArrayandError` and such, which cannot be extended (with correct functionality anyway) in standard ES5

That makes sense for built-ins, but is there some reason this enforcement cannot be applied for those constructors (perhaps by them) and not all constructors?

(btw - thanks for explaining the point of this - I was not aware of that)

Best, Don

Don Griffin Director of Engineering Sencha, Inc. www.sencha.com

# James Treworgy (7 years ago)

At which point you've lost your logger, right?

That's up to the constructor that got passed in - the only job of "createInstance" is to make sure that the client constructor has the dependencies available to it in some way, ideally one that doesn't require writing boilerplate in every class. If a constructor chooses to return something other than it's own instance, I don't really care whether or not it exposes logger, because those dependencies are only for its own use anyway.

The actual pattern might not be to assign the deps to a public property anyway, it could be assign them all to this[_someSymbol] or whatever. The only goal is to make them already available to it, in whatever convention we choose in the app, without having to pollute the actual constructor with a "deps" argument and boilerplate to assign them in every class.

So I get that this decision has to do with making builtins extensible, but I don't totally understand why it has to apply to everything, or even why we can't just allow people mis-using the builtin constructors to have strange behavior, as we do with Array in ES5.

That is, if I was simply allowed to access "Cotr" as a function, why would anything else cease to work? We remove some risk of abuse, but seems a bit like we threw the baby out with the bathwater. We could still have the class syntax work the same way, but not prevent people from accessing their own constructor functions.

I mean, nothing's stopping me writing classes like this:

class Thing { constructor(...args) { Thing.cotr.call(this, ...args) } }

Thing.cotr = function() { ... }

Now I have access to my constructor function. I could just write a babel plugin to convert all my classes to this syntax :) But it all seems so unnecessary

# James Treworgy (7 years ago)

.. heh well I guess if I was going to write a babel plugin to deal with this I'd just have it not do the "new" check anyway :) this really only applies to the actual spec / native implementation.

# Allen Wirfs-Brock (7 years ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20170105/bcb2953c/attachment-0001

# James Treworgy (7 years ago)

Thanks, that's pretty interesting. Curiously, my use case isn't part of the discussion - the proposals are focused on new syntax to deal with the "99.9%" case of wanting a factory. Not really addressed is simply "people may want to call it for some other reason", I guess I'm the 0.1% case.

It seem to me that allowing invocation through function.call or function.apply would be pretty reasonable. Using these mechanisms is very explicit. You would not lose the protection gained by prohibiting normal new-less calls, and it wouldn't require creating any new syntax to expose the function.

"YK: lets go back to es5, a massive hazard to forget use. This made many people not want the newless behavior."

This is fine (though not sure I agree it was such a massive hazard in practice - linters have done a fine job of letting us know when we screwed up for a long time) - but it seems that prohibiting invication through any and all means is overkill - and has negative consequences for advanced use cases and interop with ES5.

"MM: we should drop this completely, and solve it with decorators."

I'm not sure what that would look like but I think it only refers to newless construction..

# Don Griffin (7 years ago)

Thanks for the link. It seems that discussion is regarding an aesthetic concern of "foo = Foo()" being allowed when today "foo = new Foo()" is required. I assume these are entangled concerns but the issue James mentioned with DI and I've hit with multiple-inheritance are the restriction on ".call()" and ".apply()" being used on constructor functions.

Is there a way to separate these? I see the use of "foo = Foo()" as a cosmetic (and therefore perhaps not worth a lot of effort) while the other holds more fundamental implications for DI and MI solutions.

Best, Don

Don Griffin Director of Engineering Sencha, Inc. www.sencha.com

# T.J. Crowder (7 years ago)

On Thu, Jan 5, 2017 at 7:21 PM, James Treworgy <jamietre at gmail.com> wrote:

Can you clarify what prevents it from being made to work?

The fundamental feature difference (continuing to think about this!) is that with ES5 constructors, I can create an instance of something from an abitrary constructor provided to me, and inject properties that will be available to it at contruction time.

and

On Fri, Jan 6, 2017 at 3:49 AM, Don Griffin <don at sencha.com> wrote:

...the issue James mentioned with DI and I've hit with multiple-inheritance are the restriction on ".call()" and ".apply()" being used on constructor functions.

Thinking out loud:

Both James' DI case and the multiple-inheritance case could be addressed with a Reflect utility function that allows you to provide a hook triggered when this is being allocated for the base constructor. This is basically call for constructors, but with a hook rather than a thisArg. Just to have a name/concept for it: Reflect.new(targetConstructor, hook, ...args). For now let's assume hook is a simple function that receives the freshly-allocated this and can either augment it or return a replacement, but I'll circle back to that later.

James' example:

let o = Reflect.new(Ctor, thisObj => {
    thisObj.logger = new Logger();
});

Let's assume Ctor extends Base; it would work like this:

  1. Call EvaluateNew(Ctor, args, hook), note the new third argument. EvaluateNew passes the hook to Construct, which passes it to [[Construct]].
  2. Eventually during that [[Construct]], Ctor calls super, which calls Base's [[Construct]], also passing in the hook. (I don't know yet how super(...) has access to the hook; I guess we'd have to have it on the environment, like [[NewTarget]].)
  3. We reach Step 5 of Base's [[Construct]] call. Since Base's kind is "base", we perform OrdinaryCreateFromConstructor, but then pass the result through the hook. Since this particular hook doesn't return an object, thisArgument is set to the result from OrdinaryCreateFromConstructor as usual.
  4. Construction completes as normal.

So even the base constructor sees James' logger property on this by the time it has a this, because the hook gets a chance to augment it before the base constructor code runs.

An MI example with A, B, and C, assuming they're all base constructors:

let o = Reflect.new(C, () =>
    Reflect.new(B, () =>
        new A()
    )
);
// (Presumably do some mixing in of prototype properties...)

Execution is like before, but in this case when we reach Step 5 of [[Construct]] for B and C, our hook returns an object, and so thisArgument is set to that object rather than the one from OrdinaryCreateFromConstructor.

I said I'd circle back on hook being a simple function: The MI example above creates and throws away two objects it doesn't need to (the ones created for B and C but then replaced by the hook). If that's a concern, we can make hook an object with allocate and/or postAllocate properties: allocate would provide this (MI), postAllocate would just augment it (James' DI example). Or whatever. That's jumping forward to design; we're still at the concept stage and may well never reach design.

If needed for things like Error, constructors could have a flag indicating that they cannot accept the hook (or at least, cannot accept the hook providing a different this), causing a throw at Step 5 of [[Construct]].

Conceptually simple. Not necessarily simple in terms of impacts on specification or implementations. In terms of the spec, we have at least:

  • Adding Reflect.new (or whatever it's called)
  • Modifying EvaluateNew (here)
  • Modifying Construct (here)
  • Modifying [[Construct]] (here)
  • Keeping track of the hook somewhere such that evaluating super(...) (here) can pass Construct the hook, possibly another slot on environment records
  • Possibly a flag slot or similar on functions like Error, if needed

I'm not competent to speak to impacts on implementations.

Which all sounds like a lot, but (modulo implementation complexity) I don't think it really is, and I think it's basically what we'd need to do to make call/apply work with constructors anyway (just passing around a hook function/object rather than a thisArg), since we wouldn't want call/apply to allow violating the "no this before super(...)" rule by setting the this binding early.

-- T.J.

# James Treworgy (7 years ago)

T.J. Thanks for the very thoughtful analysis. But I keep coming back to this:

since we wouldn't want call/apply to allow violating the "no this before super(...)" rule by setting the this binding early.

Why?

To me the best solution is also the simplest - just let people do this. Not only does it avoid all the implementation complexity, but it also provides feature parity between ES6 classes and conventional prototype constructor functions.

Having some mechanism (e.g. via Reflect) would be better than nothing, but it would also mean you have to write two versions of anything to support both ways of creating prototypes.

It seems like there should be a very compelling reason to prevent someone from using a feature. The risk of accidental "new" doesn't really apply to call and apply -- and even in the direct invocation situation I don't think it's compelling either (since we've had that since day 1 and managed to get by :) but I don't really care if that's prevented.

# T.J. Crowder (7 years ago)

Note: Related discussion of calling class constructors here: esdiscuss.org/topic/determine-if-a-value-is-callable-constructible

On Fri, Jan 6, 2017 at 12:11 PM, James Treworgy <jamietre at gmail.com> wrote:

T.J. Thanks for the very thoughtful analysis. But I keep coming back to this:

since we wouldn't want call/apply to allow violating the "no this before super(...)" rule by setting the this binding early.

Why?

To me the best solution is also the simplest - just let people do this.

On first blush, a couple of reasons, but that doesn't mean they aren't addressable:

  1. I think the supposed simplicity is illusory. If you get into the mechanics of actually "just" allowing people to do it with call/apply, as I said earlier, I think it looks very like Reflect.new but with less flexbility and with the "this before super()" problem. Do you have a proposal for how to actually do it that doesn't have similar complexity? You'd have to modify call and apply, you'd still have to modify how Construct and [[Construct]] work (at the very least). Maybe you get some savings by allowing a this binding before super (you might avoid the hook slot on the environment), but my instinct is the savings are small change. But I haven't gone through the same level of analysis on it as I did with Reflect.new, so I'm open to being proved wrong on that.

    Granted using them would be simpler if you didn't care whether you're calling a constructor vs. non-constructor (e.g., you "just" use call/apply). But is it really the case that there's a lot of code that doesn't need to care? Calls and construction are very different things.

    But a win for overloading call/apply is, of course, that ES5 constructor code can subclass an ES2015+ class constructor without branching. Existing ES5 code could be fed an ES2015+ class constructor and (probably) Just Work™. But I don't know that it's a common use case outside DI frameworks, which are relatively few and thus upgradeable.

  2. Having call/apply not work the same way a function call does (modulo their actual purpose) is a significant departure from how they've worked since they were added to JavaScript originally. Now every explanation of them must not only say "They call the function setting a specific value for this" but also "and they let you bypass the restriction preventing calling constructors as functions." Okay, so the counter-argument could be that allowing them to call a constructor as a function is a by-product of their main purpose (setting this), but to me it's both different and surprising.

  3. Allowing this prior to super() in a subclass constructor depending on how it's called makes it impossible for lint tools and such to flag that up early as an error. So they can't support the majority case where that's going to be an error at runtime.

Perhaps all of those can be addressed or dismissed. I don't want to give the impression of being married to anything (not that my opinion is particularly important anyway). But my gut says overloading call/apply (thus allowing early this binding) isn't markedly simpler, but is more problematic.

Side note: In my outline of Reflect.new, I should have flagged up that we'd really want at least the Reflect.isConstructor part of this proposal to be in place. DI systems would need to know what they were dealing with. But (to me, anyway) the Reflect.isConstructor part of that proposal is non-controversial (Reflect.isCallable has an open question about what it should say for class constructors -- how relevant!).

I feel like I've done a lot of talking on this; I'm going to hang back and just listen for a while. :-)

-- T.J.

# James Treworgy (7 years ago)

Granted using them would be simpler if you didn't care whether you're calling a constructor vs. non-constructor (e.g., you "just" use

In ES5 the only thing that makes a constructor behave differently is invoking it with new. There's no difference between a constructor and a regular function, there's just a different way to invoke it. So yeah - I don't care.

Having call/apply not work the same way a function call does (modulo their actual purpose) is a significant departure from how they've worked since they were added to JavaScript original

I see your point but I don't exactly agree. It's not that they work differently, it's just that you aren't allowed to invoke once specific type of function directly. (And again if I had my way, I'd just drop the 'no new' restriction entirely for the sake of 100% backward compatibility between class vs. prototype syntax -- but I don't personally care about the new thing).

I'd make the counterpoint that the way things are now, we have a much more dramatic departure from the way things worked since the beginning: I can create a function that I have no ability to access directly through any means. There has never been a "black box" process in Javascript before, now there is. I could completely simulate the prototype chain construction process and rewire it any way I wanted, now I can't. This is a big functional and philosophical change. But mostly - from a practical standpoint - it means that "class" syntax is not backwards compatible with prototype syntax. I think that most developers would expect that with the exception of builtins that never worked correctly using the old instantiation patterns, everything else should work interchangeably. You should be able to write simple classes using simple inhertance patterns and have class just act as syntactic sugar, unless you're trying to do something that wasn't possible before.

I get that a class is not supposed to be exactly the same as a prototype constructor, since one of the goals was to fix the problem extending builtins, but it should be backwards compatible. Right now it's not in a very significant way.

The semantics of super() would need to be addressed, in the sense that

it too would have to be permitted to use an existing context in this circumstance.

This is true. Not being familiar with the implementation details I can't say how big a deal this is, but yes - the ability to pass a context down through super (the same as you did old-school by invoking the superclass constructor directly against your own context from within the subclass constructor) is required. To me, this is just backwards compatibility.

Thanks again for your time and engagement here. This is a pretty important issue to me, and I'm definitely not knowledgable about the implemenetation details. I am very gratified to be able to get into it here and learn more.