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 new
ed ([[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:
- It requires the use of ES5 function as constructors. In an ideal world, new classes would be written using class syntax.
- 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);
}
}