Allow apply() and call() of Class Constructors

The Old Way:

In the past, all classes were defined as such:

function MyClass(num) {
    this.num = num;
}

MyClass.prototype.add1 = function() {
    this.num++;
}

let one = new MyClass(1)
one.add1();

Constructors were defined as normal functions and only returned new objects when the new operator was used. When the new operator was used, the function would:

  1. Create a blank, plain JavaScript object.
  2. Add a property to the new object (__proto__) that links to the constructor function's prototype object
  3. Bind the newly created object instance as the this context (i.e. all references to this in the constructor function now refer to the object created in the first step).
  4. Run the constructor function
  5. Return this if the function doesn't return an object.

What this means (for the sake of this idea) is that the 3rd and 4th steps were essentially Constructor.apply({}, prams). This operation was also valid on its own, which is the point of this idea:

MyClass.apply(one, [5]); // Now the "num" value is 5
MyClass.addOne()

New class Declerations:

In some situations, it would be useful to use these internal steps with newer class declarations. Consider Object Pooling, where all pooled objects require a reset method. This leads to lots of classes that look like this:

class MyClass {
    constructor(num) {
        this.reset(num)
    } 
    reset(num) {
        this.num = num;
    }
}

Less readable, and not useful at all if you have 3rd party libraries. We can not do the same trick as above (reusing the constructor as a reset method), because Class constructor MyClass cannot be invoked without 'new'. This is an error thrown whenever a class is called as a function (MyClass()) which also happens when using apply and call. I suggest that this restriction be removed for cases where apply or call is used with an already created instance of that class.

See the first item on https://github.com/tc39/proposals/blob/aa528b17cb104d80c4be3f9539f47fd1486df3cc/inactive-proposals.md

That is useful if you want new Date() and Date() to do the same thing, I want the constructor to do everything except for creating a new object (which is not possible with decorators)

How would private fields be handled? Normally you can't add new private fields to an existing object. But, if I take an instance of one class, then use .call() to give that same instance to a bunch of other class constructors, are new private fields going to be installed onto the same instance? Or, would it fail as soon as a class tries to install an extra private field? This is an interesting can of worms.

I'm also not positive a change like this can be made for all existing classes. Some classes could be designed with the expectation that it will always receive an empty object as "this" when its constructor is run. A change like this could allow users to abuse classes in new way that wasn't previously possible, which normally isn't a big deal, but if that class is supposed to be part of something security-sensitive, where untrusted code is running and using these classes, we're now providing a new attack vector for users to potentially dig into the class in new ways. So, such a feature would probably have to be opt-in instead of "automatically applies to every class that's been created".

What if you limited it to only working on the original object's constructor (or one of the constructors that the object is derived from).

Is there another use case besides the one listed? That use case seems fairly niche. It's not common to want to bypass JavaScript's built-in garbage-collector like that and try and put control of garbage collection into your own hands. If you really need to do this, you can with that pattern, but from what I understand, you wouldn't use that specific pattern for many different types of classes, so it doesn't seem like a ton of boilerplate to have "constructor(...args) { this.reset(...args) }; reset(...) { ... }" in the handful of classes that need to be able to participate in this alternative memory-management system.

1 Like

Also, you might be interested in this thread, which talks about exposing an API to give hints to a garbage collection, like asking the garbage collector to refrain from running for a period of time. This could help remove some of the need for this sort of custom memory management.

It could be useful for any instance where you want to reset an object to its original state, for example resetting a transformation. It might also not be much boilerplate for you, it would be useful for old libraries that are not updated for this (and require you to redo the entire constructor inside a new reset function)

It could be useful for any instance where you want to reset an object to its original state

I actually see this more as a disadvantage. Lets take promises as an example:

const promise = doSomeAsyncTask()
givePromiseToSomeone(promise)
await promise

When you do await promise, are you awaiting the same promise that you received from doSomeAsyncTask()? Or, did givePromiseToSomeone() reset the promise instance, effectively replacing it with a different promise? Right now, you can know with a fair amount of certainty that it's the same promise (as long as odd monkey-patching isn't going on to make the one instance act like another one). It would be nice if we can keep it this way as much as possible. It's these sorts of guarantees that make it easier to read code, and know that nothing odd is happening. It would be better if we could find more ways to add restrictions to the language and have more of these kinds of guarantees (a rather difficult thing to do without breaking backwards compatibility). Allowing us to reset any class seems like a step in the wrong direction.