title |
---|
call constructor proposal |
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.
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());
}
}
The presence of a call constructor
in a class body installs the call constructor function in the [[Call]]
slot of the constructed class.
It does not affect subclasses, which 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.
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";
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);
}
}