Call constructor proposal

Stage 1 Proposal

Champions:

  • Yehuda Katz
  • Allen Wirfs-Brock

Motivation

History

ES5 constructors had a dual-purpose: they got invoked both when the constructor was newed ([[Construct]]) and when it was called ([[Call]]).

This made it possible to use a single constructor for both purposes, but required constructor writers to defend against consumers accidentally [[Call]]ing the constructor.

ES6 classes do not support [[Call]]ing the constructor at all, which means that classes do not need to defend themselves against being inadvertantly [[Call]]ed.

In ES6, if you want to implement a constructor that can be both [[Call]]ed and [[Construct]]ed, you can write the constructor as an ES5 function, and use new.target to differentiate between the two cases.

Motivating Example

The "callable constructor" pattern is very common in JavaScript itself, so I will use Date to illustrate how you can use an ES5 function to implement a reliable callable constructor in ES6.

// these functions are defined in the appendix
import { initializeDate, ToDateString } from './date-implementation';

export function Date(...args) {
  if (new.target) {
    // [[Construct]] branch
    initializeDate(this, ...args);
  } else {
    // [[Call]] branch
    return ToDateString(clockGetTime());
  }
}

This works fine, but it has two problems:

  1. It requires the use of ES5 function as constructors. In an ideal world, new classes would be written using class syntax.
  2. It uses a meta-property, new.target to disambiguate the two paths, but its meaning is not apparent to those not familiar with the meta-property.

This proposal proposes new syntax that allows you to express "callable constructor" in class syntax.

Here's an implementation of the same Date class using the new proposed syntax:

import { initializeDate, ToDateString } from './date-implementation';

class Date {
  constructor(...args) {
    initializeDate(super(), ...args);
  }

  call constructor() {
    return ToDateString(clockGetTime());
  }
}

Specification

The following changes and additions are relative the ECMAScript 2015 Specification

9.2 Table 27

The following entry is added to Table 27

Internal Slot Type Description
[[ConstructorCall]] Object or empty The function object that is evaluated when a class constructor is invoked using [[Call]]. Only used when [[FunctionKind]] is "classConstructor".

[9.2.1 [[Call]]](https://ecma-international.org/ecma-262/6.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist)

Step 2 is replaced with:

   2. If F's [[FunctionKind]] internal slot is "classConstructor", then
      a.  If  F's [[ConstructorCall]] internal slot is empty, throw a TypeError exception.
      b.  Let callF be the value of F's [[ConstructorCall]] internal slot.
   2.1 Else, let callF be F.

Step 7 is replaced with:

  7.  Let result be OrdinaryCallEvaluateBody(callF, argumentsList).

Update the step reference in the NOTE.

9.2.9 MakeClassConstructor

Between the existing steps 3 and 4 insert the following steps:

   3.1 Set F's [[CallConstructor]] internal slot to empty.

14.5 Class Definition Syntax

The definition for the production ClassElement[Yield] is replaced with:

ClassElement[Yield] :
    MethodElement[?Yield]
    static MethodElement[?Yield]
    CallConstructor
    ;

CallConstructor :
    call constructor ( StrictFormalParameters ) { FunctionBody}

14.5.1 Early Errors

Add the rules:

ClassElementList : ClassElementList ClassElement

  • It is a Syntax Error if ClassElement is CallConstructor and ClassElementList Contains CallConstructor.

CallConstructor : call constructor ( StrictFormalParameters ) { FunctionBody}

  • It is a Syntax Error if any element of the BoundNames of StrictFormalParameters also occurs in the LexicallyDeclaredNames of FunctionBody.
  • It is a Syntax Error if StrictFormalParameters Contains SuperCall.
  • It is a Syntax Error if FunctionBody Contains SuperCall.

Add 14.5.x StaticSemantics: CallConstructorDefinition

ClassElementList : ClassElement
   1.  If ClassElement is not CallConstructor , return empty.
   2.  Return ClassElement.

ClassElementList : ClassElementList ClassElement

   1.  If ClassElement is  CallConstructor , return ClassElement.
   2.  Return CallConstructorDefinition of ClassElementList.

14.5.5 Static Semantics: ComputedPropertyContains

Add the rule:

ClassElement : CallConstructor

   1.  Return false.

14.5.9 Static Semantics: IsStatic

Add the rule:

ClassElement : CallConstructor

   1.  Return false.

14.5.10 Static Semantics: NonConstructorMethodDefinition

In the algorithm for the rule ClassElementList : ClassElement add the following new step between the existing steps 1 and 2:

   1.1  If ClassElement is the production ClassElement : CallConstructor , return a new empty List.

In the algorithm for the rule ClassElementList : ClassElementList ClassElement add the following new step between the existing steps 2 and 3:

   2.1  If ClassElement is the production ClassElement : CallConstructor , return list.

14.5.12 Static Semantics: PropName

Add the rule:

ClassElement : CallConstructor

   1.  Return empty.

14.5.14 Static Semantics: ClassDefinitionEvaluation

Replace the existing steps 8 and 9 with:

   8.  If ClassBodyopt is not present, then
       a.  Let constructor be empty.
       b.  Let callConstructor be empty.
   9. Else,
       a.  Let constructor be ConstructorMethod of ClassBody.
       b.  Let callConstructor be  CallConstructorDefinition of ClassBody.

Between the existing steps 18 and 19 insert the following steps:

   18.1 If callConstructor is not empty, then
        a.  Let callF be FunctionCreate(Normal, StrictFormalParameters, FunctionBody, classScope, true, functionPrototype).
        b.  Set F's [[CallConstructor]] internal slot to callF.

The following informative NOTE is added:

NOTE The function object created as the value of callF is not observable to ECMAScript code. MakeMethod is not applied to that function object, because the F's [[HomeObject]] binding is used when invoking the [[CallConstructor]].

Remarks

The presence of a call constructor in a class body installs the call constructor function in the [[CallConstructor]] slot of the constructed class. The [[Call]] internal method of a class constructor invokes the [[CallConstructor]] function.

The function object value of [[CallConstructor]] is not intended to be ovservable by ECMAScript code. If any features are added to ECMAScript that exposes the "current function" that such features should expose the constructor object and not the [[CallConstructor]] object.

The presence of a call constructor in a superclass does not affect subclasses. This means that subclasses still have a throwing [[Call]], unless they explicitly define their own call constructor (subclasses do not inherit calling behavior by default).

As in methods, super() in a call constructor is a static error, future-proofing us for a potential context-sensitive super() proposal.

Appendix: Date Utilities

import { clockGetTime } from "system/time";
import Type, { OBJECT, STRING } from "language/type";

// the spec makes these things implementation-defined
import { parseDate } from "host";

// see the next appendix
import { InternalSlots } from "self-hosted";

// define the private slot for Date, which contains a single field for the value in milliseconds
export class DateValue {
  constructor(timeValue: number) {
    this.timeValue = timeValue;
  }
}

const PRIVATE_DATE_FIELDS = new InternalSlots(DateValue);

export function privateDateState(date) {
  return PRIVATE_DATE_FIELDS.get(date);
}

export function initializeDate(date, ...args) {
  switch (args.length) {
    case 0:
      return initializeDateZeroArgs(date, clockGetTime());
    case 1:
      return initializeDateOneArg(date, args[0]);
    default:
      return initializeDateManyArgs(date, args);
  }
}

function initializeDateZeroArgs(date) {
  PRIVATE_DATE_FIELDS.initialize(date, clockGetTime());
}

function initializeDateOneArg(date, value) {
  let timeValue = do {
    if (Type(value) === OBJECT && DATE_SLOTS.has(value)) {
      DATE_SLOTS.get(value).timeValue;
    } else {
      let v = ToPrimitive(value);
      Type(v) === STRING ? parseDate(v) : ToNumber(v);
    }
  }

  DATE_SLOT.initialize(date, TimeClip(timeValue));
}

function initializeDateManyArgs(date, args) {
  // TODO
}

// re-export implementation-defined ToDateString
export { ToDateString } from "host";

Appendix: Self Hosting Utilities

class InternalSlots {
  constructor(SlotClass) {
    this._weakMap = new WeakMap();
    this._SlotClass = SlotClass;
  }

  initialize(obj, ...args) {
    let { _weakMap, _SlotClass } = this;
    _weakMap.set(obj, new _SlotClass(...args));
  }

  has(obj) {
    return this._weakMap.has(obj);
  }

  get(obj) {
    return this._weakMap.get(obj);
  }
}