1 of 14

New.initialize

For Stage 1

Daniel Ehrenberg

Igalia, in partnership with Bloomberg

January 2019 TC39

2 of 14

Motivation

3 of 14

Stable prototype chains

4 of 14

ES6 subclasses

  • B invokes A, which returns this
  • Then B finishes initializing this

class A {� constructor() { this.x = 1; }�}�class B extends A {� constructor() { super(); this.y = 2; }�}��const instance = new B();�console.log(instance.x); // 1console.log(instance.y); // 2

5 of 14

Dynamic prototype chains

  • The super constructor is looked up dynamically
  • A different one is called if the prototype chain is modified!

class A {� constructor() { this.x = 1; }�}�class B extends A {� constructor() { super(); this.y = 2; }�}�B.__proto__ = class {� constructor() { this.z = 3; }�};�const xx = new B();�console.log(xx.x); // undefined!console.log(xx.y); // 2console.log(xx.z); // 3!

6 of 14

A strong defense: Freezing the constructor

  • Object.freeze prevents resetting the __proto__
  • And that means we keep calling out to the original superclass!

class A {� constructor() { this.x = 1; }�}�class B extends A {� constructor() { super(); this.y = 2; }�}�Object.freeze(B)�B.__proto__ = class {� constructor() { this.z = 3; }�}; // TypeError

const instance = new B();�console.log(instance.x); // 1console.log(instance.y); // 2

7 of 14

A subtle defense: Reflect.construct instead of super()

  • Instead of calling super, call the underlying construction:�Reflect.construct
  • Mutation doesn't matter then.

class B extends A {� constructor() {� const instance = Reflect.construct(� A, [], new.target);� instance.y = 2;� return instance;� }�}�B.__proto__ = class { };�const instance = new B();�console.log(instance.x); // 1!console.log(instance.y); // 2

8 of 14

How does this work with private fields?

9 of 14

Similar issues occur with private fields

  • Base class fields only exist when the right super constructor is called
  • Freezing still works as a defense

class C {� #v = 1;� get v() { return this.#v; }�}�class D {� #w = 2;� get w() { return this.#w; }�}�const instance = new D();�console.log(instance.v); // 1console.log(instance.w); // 2D.__proto__ = class {};�const broken = new D();�console.log(broken.v); // TypeError!console.log(broken.w); // 2

10 of 14

Applying Reflect.construct

  • Reflect.construct can call the right super constructor
  • NEW:�new.initialize installs the fields and private methods of this class

class D {� #w = 2;� get w() { return this.#w; }� constructor() {� const instance = Reflect.construct(� C, [], new.target);� new.initialize(instance);� return instance;� }�}��D.__proto__ = class {};�const fixed = new D();�console.log(fixed.v); // 1!console.log(fixed.w); // 2

11 of 14

FAQ

12 of 14

check to disable calling new.initialize twice?

  • Maybe? We could? Why not?
  • I don't see the point
  • You're not really gaining any invariants
    • The "super return trick" can do all this stuff
    • You could call the constructor multiple times
    • Etc.
  • Note: Adding the same private field/method twice throws
    • Seems redundant to have an extra check as well

13 of 14

<small>

private symbols without reification doesn't solve this issue

</small>

14 of 14

Stage 1?