tl;dr: new and super run different code for same constructor; shows the problem, proposes the solution that assures it always run the same code, giving precedence to .constructor over the function.

Hello,

in the present state of the spec, the semantic of super(...) is defined to call super.[[MethodName]](...), in every method where super is allowed.

This design is simple and straightforward, but it puts much more semantical role to the Class.prototype.constructor.

This design has its logic, when coupled with @@create, it is roughly analogous to the way of Squeak-derived Smalltalk dialect with new/initialize couple. There is class-side method #new/.@@create, which creates an instance, and _on_that_instance_ the initializing method #initialize/.constructor is called.

JS has the advantage here that it can take any number of parameters, so its .constructor can take take them unless the Smalltalk #initialize which is argument-less and so arguments must be taken care in #new. Detail, anyway.

Now, this design is half-implemented and there are inconsistencies. There should a decision be made if .constructor is really a method that initializes an instance. I will presume it is so, show the inconsistency (ehich I deem really big one), and try to propose a fix.

=== THE PROBLEM ===

  class Foo {
    constructor () { console.log(1); }
  }

  Foo.prototype.constructor = () => console.log(2);

  class Bar extends Foo {
    constructor () { console.log("a child of:"); super(); }
  }

  new Foo
  ==> 1
  new Bar
  ==> a child of:
  ==> 2

This inconsistency id really bad: the new and super() calling constructor of the same class run different code.

I am _not_going to propose freezing .constructor. On the contrary, I propose to embrace it as a constructor as much as possible.

It really boils down to the selection of "who is the constructor"? In different thread I was proposing to embrace that "Foo" be the constructor of Foo, in super as well. But this would need to treat super() in constructor specially, and it did not seem to be liked.

Coupled with @@create, I like the other way, too. But then, we should embrace the "Foo.prototype.constructor" is the constructor of class Foo; always when possible.

=== THE SOLUTION ===

The problem is that new [[Call]]s the constructor function Foo (unchanged by assignment to .constructor), but super [[Call]]s Foo.prototype.constructor.

First step is to change the semantics of [[Construct]], so that
(after creating the object with @@create) instead of placing the [[Call]] of class itself, it gets .prototype.constructor, and place a [[Call]] there.

This breaks things, when done in naive way.

So the second step is to ensure there is no break.

For this, we must distinguish the proper "@@create/.constructor in new as well as super" classes from the legacy "@@create/Class/no super" classes. That is, super should fail when called on non-proper class (I'll show later why). There should be simple way to properize legacy "classes".

So the [[Construct]] should look like this:

1) Let creator be Foo.[[Get]](@@create)
2) Let newObj be creator.call(foo). //Foo is passed as the this value to @@create
3) Let proto be Foo.[[Get]]("prototype")
4) Fail if problem.
5) If there is [[ProperClass]] in proto, then
5.1) Let ctor be proto.[[GetOwn]]("constructor")
5.2) Throw "Not constructible" if not present.
6) Else
6.1) Let ctor be Foo
7) Let ctorResult be ctor.[[call]](newObj,args)
8) If Type(ctorResult) is Object, return ctorResult
9) else return newObj

(The @@create call can be moved down so no allocation when failed to obtain ctr).

The role of [[ProperClass]] in prototype is to say "I belong to the true class, its constructor is my .constructor, therefore super(...) calls the same as new".

Also super(...) must be changed (only the unqualified one). It should first check if the prototype it should get the method from has [[ProperClass]] and fail if it does not; only then it should proceed to be the same as super.[[MethodName]](...).

The outcome is:
- legacy classes work as usual. new Foo() [[Call]]s Foo, super(...) fails to avoid incosistence - classes created by "class" work the same if you do not bother to change .prototype.constructor, but super(...) works on them. - but if you change .prototype.constructor, new as well as subclass' super unisono [[call]] the same code: the changed .constructor; and unisono fail it if is deleted or replaced by something non-callable. (I'd like to stress here calling .prototype.constructor in new is not anything crazy, novel and dangerous: see Squeak/Pharo's #new/#initialize).

The last piece of the puzzle should be the possibility to make legacy class [[ProperClass]]. It's rather easy to do it: rewire the .constructor, make .prototype non-configurable. As for how to express it, there is two possibilities:

Either an API:

Reflect.fixClass(LegacyClass[, ...]);

or the keyword:

class LegacyClass[, ...];

Herby

P.S.: This opens interesting possibilities. Like a plain object can be a class, only thing it needs is .prototype with [[ProperClass]].

P.P.S.: The class keyword also puts up constructor-inheritance, so the question is whether fix-legacy-class should not also rewire it as well, if superclass is [[ProperClass]]. I'd say yes.
_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to