Good-bye constructor functions?

Allen Wirfs-Brock allen at wirfs-brock.com
Tue Jan 8 14:24:17 PST 2013


On Jan 7, 2013, at 9:30 PM, Kevin Smith wrote:

> I think it's arguable either way.  I mean, the initializer-only behavior advocated by Allen et al makes the class useless when called (as opposed to new'd).

Not really.  If you want to, you can still do the same sort of ad hoc new/call discrimination in a class constructor that some JS programmers have historically done in constructor functions:

class Foo {
    constructor() {
       if (this===undefined) return new Foo();
       this.isFoo = true
    }
}

If you like this pattern you can still do it, plus I showed that it can still work with subclassing (including of built-ins) and super constructor calls. 

Whether this is a good or bad (or neutral) pattern is a separable issue. Also, we don't know which patterns will become widely adopted by JS programmers as class declaration come into wide use.  No doubt, at first, they will just follow either the JS patterns they already know or patterns they learned in other languages.  But in the long rung specific JS class declaration best practices will emerge.  None of us know exactly what they will be. 

> 
> I don't think such an opinionated approach is really necessary.  One can account for the funky behavior of the built-ins by providing an additional "call" hook on the class, and leave everything else as is.  When the class is called (as opposed to new'd), it would use that hook instead of the constructor.  ("super()" would still use the parent's constructor function.)

I think any stylistic opinions are secondary.  The key point is that we can account for the funky behavior of built-ins and support their subclassing without anything in addition to what we already have in the ES6 spec.  Maybe in the future we may decide to add language to support this pattern but it isn't essential that it happens now.

> 
> It could be supported with syntax like so:
> 
>     class C {
>         static(...args) { /* call behavior */ }
>     }
> 
> I think one could also make a case that the default behavior for such a hook would be to "new" the class, as some built-ins do (and as many current JS "classes" attempt to do):
> 
>     class C {
>         static(...args) { return new C(...args); }
>     }
> 
> This seems like a pretty clean design to me.

We haven't yet found consensus on any class syntax extensions beyond the basic max-min class syntax.  I suspect that it would be hard to get buy-in on the above syntax.   But let's look at how we could do it without new syntax and also examine some of the implications. 

We could accomplish pretty much the same thing via a @@call method on a class property.  To make this work we would have to make class constructors a new kind of exotic object (they currently are specified as ordinary function objects).  Their [[Call]] internal method could be defined approximately as the following JS pseudo code:

internal method [[Call]] (thisValue, argList) {
    var ctor = this;  //the constructor object this internal method was called upon
    var calledHandler = ctor.@@call;
    if (calledHandler !== undefined) {
          if (isFunction(calledHandler)) 
              return calledHandler.[[Call]](thisValue,argList)
    }
    return the result of applying the ordinary function [[Call]] internal method to ctor with arguments thisValue and argList
}

So, C() woud invoke the  C.@@call method, if it was defined or inherited. These objects would also need to have a different [[Construct]] internal method so thatnew C() would invoke C using the body provide by the constructor body in the class declaration rather than going through the revised [[Call]] internal method.. 

That gives us the split dispatch for new/call. so now let's think about what happens with super calls during evaluation of both the constructor body and a @@call method.

First, I want to clarify that currently expressions of the form super() have no special semantics within constructor bodies.  In any concise method, super() is just shorthand for
    super.<methodName>()
where <methodName> is the property key used in the concise method definition. Now consider:

import call_symbol ...;
class B {
   constructor() {console.log("B constructor")}
}
Object.mixin(B, {
    [call_symbol] () {console.log("B @@call method")}
});
class C {
   constructor() {
      super();   //or super.constructor()
      console.log("C constructor");
   }
}      

So, using the current specification of super() and the new [[Call semantics above, this expresion:
    new C()
will log: "B @@call method", "C constructor".

How do we fix this?  Presumably by giving special treatment to super() and super.constructor() (but only within concise "constructor" methods?? What should a super.constructor() call mean in other methods?).  What would that special treatment be?  It can't just be a [[Construct]] call to the result of the super-lookup  of "constructor" because that would allocated a new object, and use it as the this value which is not the semantics we want.  instead we probably need to define the semantics to be approximately:

    var super_ctor_ref = perform super lookup of "constructor" and return a super Reference to it.
    var super_ctor = GetValue(super_ctor_ref)l;
    return the result of applying the ordinary function [[Call]] internal method to super_ctor with  the current this value and an appropiate argument list.

Note that I tried to avoid adding a new internal method to deal with this case because such an addition has significantly deeper impact on both the spec. and probably implementations. However, what if the super class is a proxy?  In that case, simply trying to apply the default ordinary [[Call]] to it isn't the right thing to do.  So maybe this really does require adding a new  internal method on all callable objects.

There are other edge case issues I may go into if I need to latter.

My real bottom line, is that there is actually a lot of complexity associated with this apparently simply change and it really isn't needed to preserve existing idioms or to make subclassing work, even for built-ins.  I've always been somewhat favorably incline towards the @@call method solution, but I just don't see any urgency in adding in now.  I think that the @@create method solved the hard subclassing problems and the ones that remain can be successfully managed by ES programmers, at least for now.

Allen



> 
> { Kevin }



More information about the es-discuss mailing list