Skip to content

Instantly share code, notes, and snippets.

@wycats
Last active September 30, 2015 07:29
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save wycats/952929fab0bc1f000c24 to your computer and use it in GitHub Desktop.
Save wycats/952929fab0bc1f000c24 to your computer and use it in GitHub Desktop.
title
call constructor proposal

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());
  }
}

Semantics

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.

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);
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment