Open littledan opened 7 years ago
also see https://github.com/erights/Orthogonal-Classes/issues/3
I also think there are several issues with the concept of constructor placement/visibility as described by @erights in the proposal.
I made the following table to clarify what @erights described, but didn't put it in because I realized it didn't make much sense.
constructor visibility | member defined |
---|---|
constructor() {} | Prototype constructor property initialized to constructor function. |
#constructor() {} | No constructor property on prototype. Prototype #constructor private field initialized to constructor function. |
static constructor() {} | No constructor property on prototype. constructor property of constructor function initialized to constructor function. |
static #constructor() {} | No constructor property on prototype. Prototype #constructor private field of constructor function initialized to constructor function. |
own constructor() {} | No constructor property on prototype. constructor property of each instance initialized to constructor function. |
own #constructor() {} | No constructor property on prototype. Prototype #constructor private field of each instance initialized to constructor function. |
The case that seems like it might be most useful is #constructor(){}
as it seemingly makes the constructor privately available to to the class body without publicly exposing it as an instance accessible property. But that essentially makes the constructor a "private methods of the prototype" and the proposal already has disallowed prototype private methods methods because there isn't a good way to access them from within the class body (in particular from within other prototype methods). #constructor
on the prototype would have all of those same access issues.
static constructor(){}
(as defined in the table) and static #constructor() {}
are accessible if the class is named but really just provide a longer way to express what can already be expressed:
class Foo {
bar(){return new Foo.#constructor } //means same as `return new Foo`
}
own constructor(){}
and own #constructor(){}
don't have have the accessibility issues of a private constructor field on the prototype. But adding an extra slot to every instance seems unnecessarily expensive was a way to limit public access to a constructor via instances.
Rather than applying this full but limited utility generality to constructor
I image two alternative special case solutions for restricting access to the constructor.
1) special treatment of #constructor
Generally disallow placement prefixing of constructor
and #constructor
(except for static constructor(){}
which is currently allowed by ES2015 with no special meaning). However, allow #constructor() {}
with special case semantics that means don't add a constructor
property to the prototype (and also don't define a #constructor
field on the prototype).
2) lexical declaration of constructor function
Assuming we allow function declarations to lexically occur within a class body we could provide special case treatment for functions named constructor
. In particular, we could recognize syntax like this:
class {
function constructor() {} //define the class' function body
bar() {new constructor()}; //lexical reference to current class constructor
}
Basically, a class element that is a function declaration for constructor
would provide the function body of the class' constructor and it would lexically bind that function to the name constructor
within the body of the function. It would suppress creation of a prototype property named constructor
.
Of these two alternatives, I currently prefer the second.
It turns out I was wrong about the utility of "private constructors" expressed by any of these means anyway. It does not protect against the hazard under naive subclassing. (attn @dtribble , @fudco )
Given
class Foo {
static #constructor() {} // or any of the other proposed privacy forms
}
the Foo
constructor is indeed not accessible from direct instances of Foo
. However, if there's a later naive
class Bar extends Foo {
constructor() { super(); }
}
then the Bar
constructor us accessible from instances of Bar
. The Bar
constructor visibly inherits from the Foo
constructor. And instances of Bar
are still (indirect) instances of Foo
.
This does not argue against making the current proposal as orthogonal as possible. But it does remove this utility argument as an additional argument for this design.
I'm not sure what hazard you are concerned about. I thought the main concern was that a reference to an instance of Foo could be restricted such that it did not also grant access to Foo itself. Regardless of the syntax, all of the "private constructor" schemes that block creation of a prototype constructor
property have that characteristic.
For the subclassing case you show here, the code in question already has access to Foo and hence the ability to invoke it as a constructor. This comes with access to Foo and has nothing to do with subclassing. There are many ways that any code that has access to Foo could leak it. Why is subclassing any more of a concern than any of the other ways of leaking it?
That code has access to Foo
. That is not the problem. The problem is that instances of Bar
grant access to Foo
. If the author of Bar
intends this, no problem. Bar
's author provides only access they have a right to provide. However, it might not have been Bar
author's intention to override this aspect of Foo
's design. The problem is that this override happens anyway, when Bar
's author is just following the normal way of doing things.
Given that the #constructor
pattern doesn't get inherited as we'd hope for such a mechanism to do, would it be reasonable to leave the constructor things out of this proposal for now? Maybe we can just ban private methods and fields named #constructor
to future-proof for it in a later proposal.
@littledan Yes, I think that is reasonable. I like the future proofing point. It means we can take this suggestion by "simply" adding these other constructor
cases to our exclusion matrix for now.
Leaving this open for the moment anyway to gather more comments. But yet, I expect we will take your suggestion.
What does this wording mean, that the constructor has a property named
constructor
to point to itself, and the prototype doesn't get such a property?I believe you can already have a static method named
constructor
, and it just works like an ordinary static method--the body is not used as the constructor, and it doesn't change what happens when you [[Construct]] the class. Maybe it's web-compatible to change (certainly it's doable for browsers to count how many websites make static methods namedconstructor
) but the change should be noted.I'm not sure I understand. Does this mean that the
constructor
property would not be added to the prototype, and instead there's a#constructor
field to do the same, but otherwise the class behaves the same way? (If so, seems fine to me.)Is it OK that the private constructor pattern for blocking doesn't give you a way to access the implicit default constructor definition? I could imagine a decorator for removing
C.prototype.constructor
which wouldn't prevent the default constructor from being used. (I don't see how that decorator would introduce a#constructor
field, but I'm not sure it's necessary, given that you can just access the class with a lexically scoped variable from within the class body.)Should we also have
own constructor
too, making an own property namedconstructor
in place of the prototype's property?For all of these, I'm wondering why declaring a constructor in one of these other ways would do anything to get rid of the "prototype constructor". For ordinary method/property/field declarations, things coexist just fine. Is this all about the way that an implicit constructor is generated, that anything with the same name should squelch that process and subsume it? My intuition would've been that they're in different "namespaces" and wouldn't contravene each other that way.