Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use es6 constructor instructions with a different context

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).

like image 574
ChG Avatar asked Feb 11 '19 00:02

ChG


People also ask

What are the new features in ES6?

New Features in ES6 1 The let keyword 2 The const keyword 3 JavaScript Arrow Functions 4 JavaScript For/of 5 JavaScript Map Objects 6 JavaScript Set Objects 7 JavaScript Classes 8 JavaScript Promises 9 JavaScript Symbol 10 Default Parameters More items...

What is ES6 syntax in JavaScript?

ES6 - Syntax. Comments − Used to improve code readability. These are ignored by the JavaScript engine. Identifiers − These are the names given to elements in a program like variables, functions, etc. The rules for identifiers are − Identifiers can include both, characters and digits. However, the identifier cannot begin with a digit.

What are the methods of Math object in ES6?

ES6 added the following methods to the Math object: 1 Math.trunc () 2 Math.sign () 3 Math.cbrt () 4 Math.log2 () 5 Math.log10 ()

What is the difference between includes () and rest () methods in ES6?

ES6 allows function parameters to have default values. The rest parameter (...) allows a function to treat an indefinite number of arguments as an array: The includes () method returns true if a string contains a specified value, otherwise false: let text = "Hello world, welcome to the universe.";


1 Answers

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!

like image 140
Oliver Sieweke Avatar answered Oct 19 '22 18:10

Oliver Sieweke