To any TC39 member:
I would like to know if what has been discussed here in this thread is worth drafting into a proposal, or is the whole idea DOA?
To any TC39 member:
I would like to know if what has been discussed here in this thread is worth drafting into a proposal, or is the whole idea DOA?
I think itβd be difficult to make any changes to static private, given both the contention around it during class fields, as well as given the tradeoffs in this thread.
It does sound like the base class would have to have a syntactic way to signal that its static fields are re-installed. Firstly because existing transpiled classes would be incompatible anyway. And secondly so compilers (terser/closure) and engines can still optimise private field access e.g remove unused fields, or inline read only data. Or generate more efficient transpiled code, compared to ensuring there is a way to re-run the field initialisers.
But then if a class is knowingly opting into this then maybe it could alternatively be written in a way to avoid the issues this would address in the first place.
That's the core difficulty since the issue in question is the functionality of a publicly accessible static method that needs access to a private field not accessible on a derived class. The only realistic options are:
static constructor()
in the base, super()
in the derived static initializer, or some other approach not currently discussed.So far, option 2 is the only viable way of accomplishing this. Given the contention mentioned by @ljharb, even with a viable solution that gets past all the issues without trading away any existing features or functionality, it may still be difficult to convince the board that this is a good approach.
Despite that, I'd still like to try.
Is avoiding this
and only accessing static private members directly not an option?
Not really. Although it would seem like it should be, for cases of multiple derived classes, I need the static private data members to still be specific to the constructor from which the accessing static function was called. Otherwise, 2 derived classes with a common ancestor can corrupt each other.
Truth be told, this may very well be the scenario that proves the realistic need for some kind of "protected" support.
@rdking Here's probably a much better explanation for why the two must share the namespace. Consider this code:
class Parent {
constructor() { return Child }
}
class Child extends Parent {
static #foo = "static"
#foo = "instance"
constructor() { super() }
foo() { return this.#foo }
}
If one were to invoke new Child().foo()
, should it return "static"
or "instance"
, or should it throw from the super
call? In order to resolve this to any of those results (including the throw), you have to 1. add all the instance variables to the class itself and 2. change the behavior of super
to always check before initializing private slots when there's naming collisions. This complicates class layout implementation quite a bit, especially as these are supposed to be easily turned into positional private slots.
I need a little clarification here! how can you reference the Child constructor from the Parent class since Child is not defined prior to the Parent?
It should throw from the super call. Not because of the application of 2 different private names, but because the return value from the parent ===
the function you're returning to. Allowing this is something utterly ridiculous (albeit possible due to class
implementation semantics). This setup makes a constructor an instance of itself. I must admit my awe that a semantically inconsistent scenario like this would be used as a reason not to proceed with a more consistent and useful scenario. Oh well. Water under the bridge.
Ignoring that faux pas, this still doesn't explain why the static and instance namespaces need to be the same. From an implementation perspective, 2 different private names were generated by the class definition. If it is so important to allow for both the static and instance private names to be applied to the same object, then it should have been a SyntaxError
for them both to be given the same friendly name.
The fact that this is not the case shows that it is not the intention for them to be applied to the same object. Therefore it should be the case that returning the calling function from a call to super
should throw. The fact that this is not the case shows that TC39 failed to fully consider the implications of the logical inconsistencies in both class
and fields, especially where inheritance is concerned.
Since the 2 classes are defined sequentially with no code in between, by the time the constructor of Parent
is called, Child
has already been defined within the scope. So it is available to Parent
. Chalk this up to the differences between dynamic and static languages.
Thanks for explaining that; I wasn't even aware of such a mechanics.
Bindings are computed at runtime and Child
is declared before the Parent
constructor is invoked. I could've also written it this way and used new Child(Child).foo()
to similar effect:
class Parent {
constructor(v) { return v }
}
class Child extends Parent {
static #foo = "static"
#foo = "instance"
foo() { return this.#foo }
}
Assuming you're right, and that kind of thinking was the reason behind the related unfortunate decisions, it leaves me wondering whether or not anyone is using such a construct in actual production code. I'm not so much thinking about the so-called super return trick itself but rather about using it to set a function as an instance of itself. Try as I might, I can't seem to come up with any useful reason for doing. Even the useless reasons I came up with still have better and easier ways of achieving the same level of uselessness.
Who knows. Maybe I hit a limit in my own imagination....
Probably not? But since those semantics inevitably fall out of the return override trick, it would be far worse to special-case it.
I don't understand the reasoning. Special cases with undesirable consequences are usually labeled "foot guns" and avoided like the plague, even to the point of forcing future revisions of the language to accept new foot guns in the name of avoiding old ones. What did I miss?
Right now static private fields are simpler than instance private. This can be demonstrated by looking at their downleveled equivalents.
Static private do not require WeakMap
, they are effectively the same as a locally scoped let
.
I imagine this allows for less complexity in the JIT&IC implementations which could be seen as a net positive over having more complex static field semantics. Perhaps there were not enough compelling use-cases to tip the balance.
Anecdotally I donβt come across much static private usage, I find instance private more common. I should see if a corpus analysis backs this up.
That by itself is a design issue to me. The design of private fields should have been consistent regardless of the object being used. To my memory, there were no less than 3 alternatives submitted that proposed let
semantics for instance-private fields. Needless to say, that didn't get anywhere despite the fact that it would have resulted in less foot guns than what exists in the current proposal. Likewise, static-private fields could have easily been implemented with WeakMap
semantics. However, doing so wouldn't have provided any advantage over the current let
semantics save for the ability to explain both with the same model.
Like you, I don't think static private is often used among ES developers. I don't think most ES developers see much utility in either class
, inheritance
, or properties on a function. Compound that with the "immutable" movement, and the "FP" movement, and you get a good explanation as to why. I'm just a pedantic developer who will use any tool I see a good and proper use for. When those tools fail to meet certain consistency requirements, I'm left to question them. Too much about fields in general, and private fields in specific failed to meet those consistency requirements.
I got bitten today by the same unexpected thing and it was quite convenient code to me ... here example and after the thinking behind:
class Why {
static #map(why) {
return why.#map || (why.#map = new Map);
}
#map = null; // lazy initialized
add(key, value) {
Why.#map(this).set(key, value);
}
get(key) {
return this.#map?.get(key);
}
has(key) {
return this.#map?.has(key);
}
}
The thinking
add(key, value)
is used, if everI have scrolled a bit but this particular post interested me: is the conclusion that JS won't ever have optimized fields per class so that using 10 private fields on the instance instead of the class level benefits literally zero at runtime?
Thanks!
No, you can't conclude this.
Private fields have the same conceptual semantics as WeakMaps, but that does not mean they must be implemented in the same way. And indeed private fields are not usually literally implemented with a WeakMap in engines.
You should not reason about performance characteristics of features of the language from their abstract semantics - write a benchmark and run it in the engines you care about. From my (possibly outdated) knowledge of the implementations of each, I would expect that a small number of private fields would usually be faster than a lazy-initialized Map in a private field, but with a very large number of fields (probably at least in the hundreds) that might switch over. But I couldn't actually tell you that for sure without running the benchmark.
Everything else in your post aside, this is definitely true. Private static fields are only created once per class and do not require operations on every class instance. But your example code has both a private static field and a per-instance field under the same name (which is a syntax error).
Some posts I've enjoyed on this subject: