8

Is it possible to use es6 constructor instructions on another instance by changing the "this" context (call, apply or other)? This is possible using es5 "classes". Here is a small example of what I mean:

function ES5() {
  this.foo = 'foo';
}

class ES6 {
  constructor() {
    this.bar = 'bar';
  }
}

var a = new ES6();
ES5.call(a);
console.log(a.foo + a.bar); //foobar



var b = new ES5();
//Reflect.construct(ES6); ??
ES6.call(b); //TypeError: Class constructor ES6 cannot be invoked without 'new'

console.log(b.foo + b.bar); //how to get foobar here too?

Edit: My question has nothing to do with the new keyword. The answer I'm looking for is how to run the instructions placed in an es6 constructor using another "this" context (with or without the new keyword).

ChG
  • 349
  • 1
  • 3
  • 13
  • 1
    I think this question should not have been closed – Lucas Feb 11 '19 at 01:16
  • @Chg The answer to "Is it possible to use es6 class constructor instructions on another instance by changing the "this" context (call, apply or other)?" is no. Suitable workarounds will depend on the use case. – traktor Feb 11 '19 at 14:43
  • I think your issue is you're trying to make an `ES6` object without calling the constructor. did you try `apply` instead of `call`? – Glenn Ferrie Feb 11 '19 at 17:51
  • 2
    Why do you want to do this? A popular criticism of JavaScript has been that functions and `this` are confusing because functions fulfill multiple roles. This was responded by introducing more special purpose constructs, such as classes, which can only be used in specific ways. If you need to do something that classes cannot do, don't use a class. – Felix Kling Feb 11 '19 at 18:32
  • @FelixKling I don't want to do this, but please just trust that I need to and there is no way around it in my specific situation as I have no control over the ES6 class. – ChG Feb 11 '19 at 20:03
  • The only thing I could think of is creating another class that extends the one in question. `class ES5 extends ES6 { constructor() { super(); this.foo = 'foo'; } }`. – Felix Kling Feb 11 '19 at 21:25
  • 1
    This seems like an [XY problem](http://xyproblem.info/). Instead of asking for help with an attempted solution to your problem, tell us about your actual problem. What exactly are you trying to do? – Aadit M Shah Feb 22 '19 at 14:51

1 Answers1

7

As the comments and yourself have pointed out, trying to invoke class constructors with a custom this context is really not something you want to attempt if there is any way around it. This was made hard intentionally!

If for some reasons this is unavoidable enough to justify tricky workarounds, you can find two partial solutions below. They are both imperfect in their own ways - depending on your exact situation one of them may still fit your needs.


Workaround 1

While it is impossible to set this directly in a constructor call, it is possible to set the prototype of this to an object of your choice.

To do so you can use Reflect.construct() to call the internal [[Construct]] method with a custom new.target value. this will then get initialised to an object inheriting from new.target.prototype.

Building on your example:

function ES5() {
    this.foo = 'foo';
}

class ES6 {
    constructor() {
        this.bar = 'bar';
    }
}

let b = new ES5();

function TemporaryHelperConstructor() {}
TemporaryHelperConstructor.prototype = b;

b = Reflect.construct( ES6, [], TemporaryHelperConstructor ); // The third argument corresponds to the value of new.target

console.log( b.foo + b.bar ); // foobar !

(The exact workings of Reflect.construct() and the internal [[Construct]] method are described in sections 26.1.2 and 9.2.2 of the specs)

Potential Issues

  • The class constructor is not actually called with this bound to b, it is called with this bound to an empty object directly inheriting from b. This may lead to problems if you or the class constructor rely on methods like Object.getOwnPropertyNames(), Object.getPrototypeOf() etc.

Workaround 2

While it is impossible to invoke the internal [[Call]] method of a class constructor without causing a TypeError, it is possible to extract the code block attached to the class constructor and create an ordinary function out of it, which you may then call with a custom this binding.

You can use the Function.prototype.toString() method to extract the code block of the class constructor as a string. The Function() constructor can then make an ordinary function out of this string, which you may call with a custom this binding through Function.prototype.apply().

Building on your example:

function ES5() {
    this.foo = 'foo';
}

class ES6 {
    constructor() {
        this.bar = 'bar';
    }
}

const b = new ES5();

const constructorBody = ES6.toString().match( /(?<=constructor\(\) ){[^}]*}/ )[0]
const ordinaryFunction = Function( constructorBody )

ordinaryFunction.apply( b ); // No TypeError

console.log( b.foo + b.bar ); // foobar !

Note that this snippet uses an extremely simplified regular expression for demonstration purposes. To make things robust you would need to take into account nested curly braces and curly braces in strings and comments. You would also need to extract the constructor arguments if they are needed.

(According to section 19.2.3.5 of the specs, you can rely on a consistent enough output of Function.prototype.toString() for this approach to work across implementations.)

Potential Issues

  • new.target will be set to undefined when executing the ordinary function (as is always the case with [[Call]] invocations), which may cause problems if the class constructor was using it.
  • Closures of the original class constructor will be lost to the new function created with Function() (MDN), which may cause ReferenceErrors if the class constructor was relying on them.
  • This approach will lead to a SyntaxError if applied on a derived class using super(), which is not valid syntax in ordinary functions.

Conclusion

There is no perfect solution to your problem. If your use case is simple enough, you may however still be able to achieve what you want. The partial workarounds will come with pernicious issues of their own - tread with care!

Oliver Sieweke
  • 1,812
  • 2
  • 14
  • 23
  • Thanks Oliver! What is the purpose of the Proxy in your second workaround? It seems to work just fine without it as ordinaryFunction.apply(b) gives me the desired result. – ChG Feb 23 '19 at 15:16
  • @ChG Yes, you're right. I guess my thought was that you could simply use `ES6Proxy` as your replacement class and also do whatever other things you might have wanted with it. I’ve taken it out as it was probably complicating things more than necessary without adding much to the understanding. – Oliver Sieweke Feb 23 '19 at 15:46
  • None of those solutions are applicable to my specific situation, but this is the answer to the question! Thanks! – ChG Feb 26 '19 at 13:45