6

I'm trying to do hot code reloading with ES6 classes. I need to be able to modify a classes' constructor, without replacing the class with a new one (because other people may have a reference to it).

However, I'm finding that it looks as if the class object has some internal reference to the constructor it was originally defined with; instantiating the class with new doesn't look up either constructor or prototype.constructor on the class.

Example:

class OldC { constructor() { console.log("old"); } }
class NewC { constructor() { console.log("new"); } }

OldC.prototype.constructor = NewC.prototype.constructor;
OldC.constructor = NewC.constructor;
new OldC();

---> "old"

(Updating all the other methods works fine; it's just the constructor I'm having trouble with.)

Thinking that the constructor might be being found via [[prototype]], I've also added this:

Object.setPrototypeOf(OldC, Object.getPrototypeOf(NewC));
Object.setPrototypeOf(OldC.prototype, Object.getPrototypeOf(NewC.prototype));

That doesn't help either (and I wouldn't have expected it to given that no subclassing is happening).

After all this, inspecting OldC shows that the prototype properties are exactly as I would expect them to be, with OldC.prototype.constructor being the new one. But still, constructing an instance of OldC calls the original constructor.

What's going on, and how can I fix it?

David Given
  • 13,277
  • 9
  • 76
  • 123
  • What is `OldObject` in your example? *"instantiating the class with `new` doesn't look up either `constructor` or `prototype.constructor` on the class."* No, because you use `new` on a function object and it's the function object itself that is invoked. – Felix Kling Mar 18 '19 at 21:23
  • Sorry, typo. Fixed. – David Given Mar 18 '19 at 21:28
  • *"I need to be able to modify a classes' constructor, without replacing the class with a new one (because other people may have a reference to it)."* I think the only way is to have the constructor itself delegate to another function which you can replace. – Felix Kling Mar 18 '19 at 21:28
  • 1
    The question boils down to: "can I mutate a function". See [this answer](https://stackoverflow.com/a/24539482/5459839) – trincot Mar 18 '19 at 21:29
  • In JS, a "class" isn't its own thing. It's just syntax sugar for creating a constructor function. So the `.constructor` of both "classes" is `Function`. – ziggy wiggy Mar 18 '19 at 21:32
  • ES6 classes are not _strictly_ functions, though; they can't be called, for a start, only constructed... – David Given Mar 18 '19 at 21:49
  • @DavidGiven: That's right, they are still functions though. Since ES2015 functions can either be *callable* or *constructable* or both. – Felix Kling Mar 18 '19 at 21:54

1 Answers1

3

Still, constructing an instance of OldC calls the original constructor.

Yes, because OldC itself is the constructor function. You need to overwrite the OldC variable to change what new OldC does.

Modify a classes' constructor, without replacing the class with a new one (because other people may have a reference to it).

As @trincot pointed out in the comments, you cannot modify the code of a function. You must replace the constructor with a new one, there's no way around it.

You can keep the prototype object though (which is mutable), as this is what most other things (especially the old instances) will reference.

NewC.prototype = OldC.prototype;
NewC.prototype.constructor = NewC;
OldC = NewC;

People who took a reference to the old constructor that cannot be changed now can't be helped. Your best bet is not handing out the class itself at all, but only a factory whose behaviour your update code knows to modify.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Unfortunately, people who take a reference to the old class _have_ to be helped, because it's a non-negotiable requirement for this project. Replacing it with a factory is also unlikely to work because `class Subclass extends superclassFactory()` will take an implicit reference to `class Superclass`, and superclass' constructors aren't looked up via the prototype either. It might be possible to do something really gross by extending a Proxy instance; I'll need to experiment. – David Given Mar 18 '19 at 21:55
  • @DavidGiven I'm not sure how you are doing the hot code reloading, but to help people who took a reference to the old constructor you probably need debugger access to the runtime to identify and change all these references. If you want to do it with code from inside the environment, there's nothing you can do but use `constructor(...args) { this.init(...args); }` in the initial code, and then replace the init method. – Bergi Mar 18 '19 at 22:04
  • I'm actually using TypeScript, so I could install a custom transformer to do precisely that. It's probably not as gross as abusing Proxy, too. **Edit:** Except that way I'd lose all the useful TypeScript constructor extensions... – David Given Mar 18 '19 at 22:39
  • @DavidGiven Ah, if you can integrate this in your build chain then this is the perfect solution. I'd recommend not to name the method `init` though, more something like `Symbol.for("hotreload-actualconstructor")` :-) Or maybe not even use a method at all, but just a call a local variable that your reloader can access and change. – Bergi Mar 18 '19 at 22:43
  • I made this work. Essentially I abandoned to ES6 class structure entirely, falling back on the old-fashioned JS model of a class being a function constructor with a prototype chain; the constructor simply calls `__constructor` on the new object. As that's an ordinary method I can update it. Then there's a pile of vile TypeScript transformers to rewrite TypeScript class declarations into this model. It passes all my tests so far, which I find really surprising. – David Given Mar 20 '19 at 21:45