new snd super running different code, proposal to fix.

Mark S. Miller erights at google.com
Fri Dec 7 10:50:14 PST 2012


The current behavior is needed to enable to following repair:

------- some existing buggy library --------
class Foo ....;
---------

------ some importing and repairing library ------
const BadFoo = Foo;
Foo = function .....;
Foo.prototype = BadFoo.prototype;
BadFoo.prototype.constructor = Foo;
----------

After this, instances of BadFoo and also instances of the repaired Foo
constructor, but these instances perceive the repaired constructor as
being the real one. Normally, after this repair, BadFoo is no longer
reachable from these instances.

SES does exactly this to hide and replace the original Function constructor.

On Fri, Dec 7, 2012 at 10:24 AM, Allen Wirfs-Brock
<allen at wirfs-brock.com> wrote:
>
> On Dec 7, 2012, at 2:58 AM, Herby Vojčík wrote:
>
>> 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.
>
> Short response.  The problem you identify (inconsistent behavior if Foo.prototype.constuctor is not the same object as Foo) only arises when using ES6 class definitions if the program tampers with the value of Foo.prototype.constructor after the class is created (note that Function.prototype is frozen, so it can't be tampered with).
>
> Personally, I don't see this as a great hazard.  However, if there is general concern about this issue the simplest solution is simply freezing Foo.prototype.constructor when the class definition is instantiated.
>
> We already freeze Foo.prototype and I originally proposed that class declaration should freeze Foo.prototype.constructor.  However, when TC39 reviewed the class design, it chose to not freeze it.  This was motivated by a desire to match the current conventions of the Chapter 15 built-ins and a reluctance to unnecessarily lock-down anything without first identifying a strong reason for doing so.  A strong case for enforcing a Foo.prototype.constructor is Foo invariant  (particularly in the presence of super constructor calls) wasn't discussed when when we made the decision to not freeze Foo.prototype.constructor.  I think your concern is sufficient to revisit that decision.  I'll bring it up at the next meeting.
>
> Allen
>
>
>>
>> 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
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss



--
    Cheers,
    --MarkM


More information about the es-discuss mailing list