new snd super running different code, proposal to fix.

Herby Vojčík herby at mailbox.sk
Fri Dec 7 02:58:19 PST 2012


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.


More information about the es-discuss mailing list