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