A proposal for TC-39
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
README.md

README.md

%constructor%.construct() method

Problem

Quite a bit of existing javascript code uses old function based objects:

function Foo(a, b, c) {
  this.a = a;
  this.b = b;
  this.c = c;
}

var foo = new Foo(1, 2, 3);

To enable inheritance, the prototype chain is manipulated, and such pseudo-constructors are chained. For example, within Node.js:

const util = require('util');

function Foo(a, b, c) {
  this.a = a;
  this.b = b;
  this.c = c;
}

function Bar(a, b, c) {
  Foo.call(a, b, c);
}
util.inherits(Bar, Foo);

With the introduction of Classes in the language, this pattern is broken.

While the following works:

function A(a) {
  this.a = a;
}

class B extends A {
  constructor() {
    super(1);
  }
}

const b = new B();
console(b.a);     // 1

The following does not:

class A {
  constructor(a) {
    this.a = a;
  }
}

function B() {
  A.call(1);
}
util.inherits(B, A);

const b = new B();  // Error

This is because calling A without new or super is not permitted.

The Reflect.construct() method has been suggested as a path forward, but that only partially solves the problem, consider:

const util = require('util');

class A {
  constructor(a) {
    this.a = a;
  }
}

///

function B(a) {
  if (!(this instanceof B))
    return new B(a);
  return Reflect.construct(A, [a], new.target);
}
util.inherits(B, A);

B.prototype.foo = function() {};

// This all works fine
const b = B(1);
console.log('b', b.a);
console.log('b', b.foo);
console.log('b', b instanceof B);
console.log('b', b instanceof A);

///

// This, however, does not

function C() {
  if (!(this instanceof C))
    return new C();
  B.call(this, 2);
}
util.inherits(C, B);

B.prototype.foo = function() {};


const c = C();

Here, the call to C() will throw because new.target in B() will be undefined.

The example can be modified to account for the undefined new.target, but doing so breaks the propagation of this:

const util = require('util');

class A {
  constructor(a) {
    this.a = a;
  }
}

///

function B(a) {
  if (!(this instanceof B))
    return new B(a);
  return Reflect.construct(A, [a], new.target || this.constructor);
}
util.inherits(B, A);

B.prototype.foo = function() {};

// This all works fine
const b = B(1);
console.log('b', b.a);
console.log('b', b.foo);
console.log('b', b instanceof B);
console.log('b', b instanceof A);

///

// This, however, does not

function C() {
  if (!(this instanceof C))
    return new C();
  B.call(this, 2);
}
util.inherits(C, B);

B.prototype.foo = function() {};


const c = C();
console.log(c.a);   // undefined!
console.log(c.foo); // [Function]

Essentially, there is no way of propagating this from C() to the constructor of A, even when using Reflect.construct() and new.target.

This poses a fundamental problem when attempting to update super classes to use modern class syntax without breaking existing downstream subclass code.

Proposal: {constructor}.construct(thisArg, arguments) method

Constructor functions would expose a new construct(thisArg, arguments) method that would allow the [[Construct]] function for the constructor function to be called using the thisArg as the second argument, allowing the following pattern:

class A {
  constructor(a) {
    this.a = a;
  }
}

function B() {
  if (!(this instanceof C))
    return new B();
  A.construct(this, 1);
}
util.inherits(B, A);

B.prototype.foo = function() {};

function C() {
  // Legacy style inheritance, very common in existing Node.js apps
  if (!(this instanceof C))
    return new C();
  B.call(this);
}
util.inherits(C, B);

const c = C();
console.log(c.a); // 1
c.foo();

The construct(thisArg[, ...args]) method would exist only on constructor functions.

Alternative: Callable Constructors

Currently, class constructors are not callable. This alternative proposal would make it possible to create a callable constructor decorator... which would work essentially the same as old style inheritance with new class syntax, e.g.:

class A {
  @callable constructor() {

  }
}

function B() {
  A.call(this);
}
util.inherits(B, A);

new B();

Here, the constructor A() is callable only if it is bound to this.

A();           // Throws because there is no `this`
new A();       // Initializes `this`, calls the constructor
A.bind({})();  // Uses {} as this.
A.call({});    // Uses {} as this.

A class with a callable constructor is generally identical to other classes in every other way with the notable exception that a class with a callable constructor may only extend from a constructor function or another class with a callable constructor.

That is, the following would result in a SyntaxError because class A {} does not have a callable constructor:

class A {}
class B {
  @callable constructor() {
    super();
  }
}
function C() {
  B.call(this);
}
new C();

The following, however, would work just fine:

class A {
  @callable constructor() {}
}
class B {
  @callable constructor() {
    super();
  }
}
function C() {
  B.call(this);
}
new C();

The following would also work:

class A {
  @callable constructor() {}
}

class B extends A {
  constructor() {
    super();
  }
}

new B();