Skip to content

Instantly share code, notes, and snippets.

@jorendorff
Created January 2, 2013 15:15
Show Gist options
  • Save jorendorff/4435281 to your computer and use it in GitHub Desktop.
Save jorendorff/4435281 to your computer and use it in GitHub Desktop.

Good-bye constructor functions?

tl;dr

Constructor function were workaround to specify constructor for a class without class. Now, having class nothing presribes the class being equivalent to its constructor. It always brought confusion and now with constructor empowered, it brings two-space problem. The class keyword can return plain objects, which have [[Construct]] calling the constructor of the class.

Problems

Constructor functions were important and interesting design decision of early ECMAScript. It made the design of the language minimal, while still allowing something-like-classes. But they are:

  • hard to swallow for newcomers;
  • a hack to specify a constructor for a class without having specific construct for it (an elegant one, I admit).

And there is .prototype.constructor. It was neglected in legacy code, mostly. But in ES6, it is promoted and is going to have more power. This leads to ambiguity - who is the "true" constructor of the class Foo - is it Foo or Foo.prototype.constructor?

This is the real problem - as things stand now,

  • for new the constructor is Foo;
  • for super the constructor is Foo.prototype.constructor.

It is true they are synchronized at the beginning, but they may desynchronize and then super will perform different code than new does, though common sense tells super should run the same initialization code as new for the superclass does.

This is classical two-space problem. But, when looking outside the box, it brought to me an important question: "Why do we need to replicate 'class is constructor function' legacy with class at all?"

Constructor does not need to be a function

Really, if you think about it, there is no real reason for [[Construct]] and [[Call]] to be coupled. There was one: in pre-class times, it was the way to specify a [[Construct]] for the class. But now, apart from mental legacy, there is no reason for this.

On the contrary, I think class should break this binding. By breaking it, it is more future-friendly to the fact the 'constructor' is in fact 'an object that has [[Construct]]', not 'a function that has a [[Construct]]' (why to constrain to function?). I'd even say, in a world with class, tying "Foo used in new Foo" with "Foo must be the same as Foo.prototype.constructor" is, imo, an antipattern (why, it's ridiculous, Foo is Foo, an object representing the class and holding its static bindings; its constructor is its initializing method); so I claim that if class Foo .. returns prototype's constructor member set up as legacy constructor function (as opposed to it being normal method) as the value ofFoo, it in fact promotes a tightly-coupled antipattern.

There is a little shift in semantics that would decouple those two: let class return proper object (inheriting from Object.prototype or constructor specified in extends), empty, with having only prototype and, most importantly, [[Construct]] set up. What would break if class would return 'plain object constructor' instead 'legacy function constructor'?

  • new Foo would work fine (with [[Construct]] slightly changed to call .prototype.constructor instead of F),
  • super would call the same code as new,
  • static bindings of Foo would work (it is an object, it can have properties),
  • monkey-patching Foo.prototype would work, methods are dynamically changed,

... all in all, everything works the same as before. No-one really needed the fact that consttuctor is a function, except:

  • Foo.call(this, ...) legacy subclassing would fail. super can be used instead, and if not, people learn that to manually subclass class Foo, one must Foo.prototype.constructor.call(this, ...) instead.

In spec, lot of things are already halfway there:

Spec

First, this part:

9.2.4 IsConstructor

The abstract operation IsConstructor determines if its argument, which must be an ECMAScript language value or a Completion Record, is a function object with a [[Construct]] internal method.

  1. ReturnIfAbrupt( argument ).
  2. If Type( argument ) is not Object, return false.
  3. If argument has a [[Construct]] internal method, return true.
  4. Return false.

is in fact checking for generic 'object with [[Construct]]'. Even if its prose is saying "determines if its argument [...] is a function object with a [[Construct]] internal method". I would vote for not fixing it by adding function check, but to correct prose by s/a function obj/an obj/.

Sadly, there are more places in spec, which specify what constructor is by 'a function with [[Construct]]', but they are proses, which can be fixed (4.3.4, Table 9 and more).

Then there is:

13.5.2 Runtime Semantics: MakeConstructor Abstract Operation

The abstract operation MakeConstructor requires a Function argument F and optionally, a Boolean writablePrototype and an object prototype. If prototype is provided it is assume to already contain a "constructor" whose value is F. It converts F into a constructor by performs the following steps:

  1. Let installNeeded be false.
  2. If the prototype argument was not provided,then
   a. Let *installNeeded* be **true**.
   b. Let *prototype* be the result of the abstract operation ObjectCreate.
  1. If the writablePrototype argument was not provided,then
   a. Let *writablePrototype* be **true**.
  1. Set F’s essential internal method [[Construct[[ to the definitions specified in 8.3.19.2.
  2. If installNeeded, then
   a. Call the [[DefineOwnProperty]] internal method of *prototype* with arguments "constructor" and Property Descriptor {[[Value]]: *F*, [[Writable]]: *writablePrototype*, [[Enumerable]]: **false**, [[Configurable]]: *writablePrototype* }
  1. Call the [[DefineOwnProperty]] internal method of F with arguments "prototype" and Property Descriptor {[[Value]]: prototype , [[Writable]]: writablePrototype , [[Enumerable]]: false, [[Configurable]]: false}.
  2. Return.

Despite its prose saying "Function argument F", if this is called with F being an ordinary object and prototype set to respective prototype object produced in ClassTail, this does exactly what it should: sets up prototype and [[Construct]].

The [[Construct]] itself needs to be changed a little. For legacy constructor functions, it should stay as it is now:

8.3.19.2 [[Construct]] Internal Method The [[Construct]] internal method for an ordinary Function object F is called with a single parameter argumentsList which is a possibly empty List of ECMAScript language values. The following steps are taken:

  1. Let proto be the result of Get( F, "prototype" ).
  2. ReturnIfAbrupt( proto ).
  3. If Type( proto ) is Object, let obj be the result of the abstract operation ObjectCreate with argument proto.
  4. Else, let obj be the result of the abstract operation ObjectCreate.
  5. Let result be the result of calling the [[Call]] internal method of F, providing obj and argumentsList as the arguments.
  6. ReturnIfAbrupt( result ).
  7. If Type( result ) is Object then return result.
  8. Return NormalCompletion( obj ).

But for decoupled class, item 5 should rewrote to more items:

  • Let ctr be the result of Get( proto, "constructor" ).
  • ReturnIfAbrupt( ctr ).
  • If ctr does not have internal method [[Call]], throw TypeError exception.
  • Let result be the result of calling the [[Call]] internal method of ctr, providing obj and argumentsList as the arguments.

There are more [[Construct]] algorithms in the spec already (ordinary legacy constructor function, bound legacy constructor function), so the third one, the one for .prototype.constuctor-forwarding constructors (I'd coin some name for them; generic? new-style? class-like? standalone?), could be there as well, having their own place (8.3.20.1 with actual numbering?). One also needs to discriminate which one of the [[Construct]]s to use. As it stands, in MakeConstructor writablePrototype can be used fine. If it is true, legacy forms are being set up, if it is false, class-like is being set up.

The last big piece to spec puzzle would be updating class evaulation, so normal objects is created to represent class and passed into MakeConstructor, but I am not going to include it here, it is pretty straightfroward.

Conclusion

If class would decouple the value it produces (the class) from the constructor function, it would create cleaner and more future-friendly model of classes. Coupling class and its constructor function was a necessity in the past, but now they need not to be coupled. Class.prototype.constructor takes all the responsibility for initializing the instance. This change involves little risk (only pieces of code who actually [[Call]] the "class" for manually initializing it; super-constructor call can be replaced by super or Superclass.prototype.constructor.{call,apply}). The big spec pieces that involve creation of classes and their instances are already very near and the important pieces need just a few changes. The devil is in the details, the spec is still very 'constructor must be a function'-oriented, but this is mainly in textual parts, algorithms are already generic enough. One important reason to ponder class not being constructor function is not conserving this tight coupling which in world of ES6+ with class can be seen as antipattern; freeing the contraints now when class is new construct is much easier than doing it later, when this unnecessary coupling is repeated in class semantic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment