private vs static private

In java, this makes sense. There's a single naming context for the entire class regardless of static or instance exposure. ES doesn't have such a restriction. Since functions have always been first class objects, and instances are separate objects, they have separate naming contexts.

Further more, TC39 opted to relax this even further in the creation of private fields by putting private names into yet another context all its own. But for whatever reason, instances are being forced to share this 3rd context with the constructor. Doing so breaks the common usage expectations in ES.

--- WARNING: Rant
I get comparing how ES works to how similar things work in other languages. This is good practice. However, adjustments need to be made when importing concepts from other languages so that the imported concept conforms to the nature and base features of the target language. TC39 has failed in many ways on this front where this proposal was concerned. To claim that this was the best solution they could form a concensus on seems to speak more of the internal politics of TC39 than to the limitations of the language itself, especially in the face of the fact that there are several alternative solutions crafted as libraries in ES, some of which I wrote myself, that work using different mechanisms and achieve results more consistent with both the expected behavior of class in other languages, and the nature of ES itself.
--- End Rant

But what's done is done, right? All we can do now is try to correct for what can be corrected before there is too much entrenchment around the damage.

From a different perspective this was what happened. Private fields were added in a way that mimics the existing nature of WeakMap in the language :grinning:

To this I say, it's still only a proposal. I'm not the type to depend on external tools to implement features that are not part of the language unless I wrote the tool myself. So I don't tend to use Babel or other cross compilers for ES unless I must.

> var a = new (class {
      #test = "foo"
      run() { console.log(`class.#test = ${class.test}`); }

VM298:3 Uncaught SyntaxError: Unexpected token '.'

It's nice to see that there's a proposal on the way. However, that proposal doesn't really address the core issue I'm describing. Maybe it would help if I fully spell it out. Take this for example:

class A {
   static #spvt = 'HHGTTG';
   static run() {
      console.log(`this.#spvt = ${this.#spvt}`);
   }
}

class B extends A { 
   static #spvt = 'Alice in Wonderland';
   static run() {
      console.log(`this.#spvt = ${this.#spvt}`);
   }
}

If I call B.run() there is no issue and I get "this.#spvt = Alice in Wonderland". No surprises there. If I remove B.#spvt and try again, I'll get a TypeError. Makes sense considering I'm looking for a private field that doesn't exist anywhere. Actually, I'd probaby get a SyntaxError considering the parser probably would choke on the fact that the private name #spvt wasn't defined. This is not surprising.

Now, to see why my previous examples were surprising, let's look at the expectations:

  1. A.#spvt is a private name expected to exist on A.
  2. A.#spvt is expected to be accessible from any function that has access to the scope where A.#spvt was defined, namely any member function of A that was defined as part of the class' lexical scope.
  3. A.#spvt is a key to the actual data stored on A`.
  4. Barring the fact that private names such as A.#spvt are not storable in a variable like is the case with Symbols and strings, nothing in the above 3 points is significantly different from the concept of a property.
  5. Even if a property is not directly referenced by anything defined as a part of the lexical scope of B, calling a function of A with B as the context will correctly function.

Therefore, it makes little sense for a call to B.run() in the previous example to fail when the function begin called is A.run() and A.#spvt is being requested from that function since:

  1. B inherits all the data of A, which includes the private container referenced by private name A.#spvt.
  2. The example above proves that there is no naming conflict between A and B, and therefore:
    a. No member function of B not declared by A can ever access a private field of A as it doesn't know the name of the field.
    b. No member function of A can share the name of a private field of A with B since private names cannot be stored in a variable.

So what does this all mean? It means we lose absolutely nothing by allowing an inherited function to access the private members of a context object provided that the function knows the private names in question.

Cheeky. :stuck_out_tongue_winking_eye: I get it, but it doesn't fit. WeakMaps have absolutely nothing in common with Java's private properties or ES's object properties. This is hardly counts as an adjustment of the concept. It's more of a complete override. The goal, loosely speaking was (or should have been) to provide a way to create ES object properties that are only accessible via functions lexically defined in the same scope as the one containing the object property definition.

Here's the problem: WeakMaps are the most popular tool within the language that help allow developers to accomplish this. However, they don't provide the privacy part. Anything that had or was given access to the WeakMap could find the so-called private data. The actual privacy was provided for by function scopes, essentially closures.

The only thing WeakMaps provide is a means to track multiple datasets from within a single closure. That has more to do with being able to create multiple instances of a private data set than with creating a private dataset in the first place. TL;DR - wrong idea.

Inheritance around static privates was a contentious issue for class fields; the path to consensus was that they wouldn’t inherit, to avoid some of the problem cases. You’d have to dig up the notes to find the rationale, but the current case was definitely intentionally decided - it was this, or no class fields at all, and basically the entire committee found that unacceptable.

Ok. Now we're getting somewhere. Can you give me a rough time around when the discussions were had, and whether or not they're in github or somewhere else?

I’d suggest searching the notes repo. Some might be in GitHub, but the class fields proposal was unfortunately split across three repos so it might be difficult to find.

Found it in the notes from Nov '17. The issue was a (IMHO grievous) misunderstanding of the handling of static private with respect to prototype walking. The desire to keep private access from being observable was the issue. I could paste the exact conversation here but I don't see the point in doing that. Instead, I'm going to raise my own argument here in the hopes that you guys will understand and rectify this mistake.


The issue in question is with regard to the need to walk the prototype chain of the target object in order to access private members that have been inherited. The fear was over the fact that Proxy allows one to observe accesses to the prototype object. While this is true, I argue that it is of no viable merit.

To show this, let's use the following example:

class A {
   static #spvt = 'HHGTTG';
   static run() {
      console.log(`this.#spvt = ${this.#spvt}`);
   }
}

class B extends A { }

const handler = {
   get(target, prop, receiver) {
      console.log(`Caught access to "${prop}"...`);
      return Reflect.get(target, prop, receiver);
   },
   getPrototypeOf(target) {
      console.log(`Caught access to prototype...`);
      return new Proxy(Reflect.getPrototypeOf(target), handler);
   }
};

let PB = new Proxy(B, handler);
PB.run();

My assertion is that assuming TC39 fixes this issue, the log will read as follows:

Caught access to prototype...
Caught access to "run"...
Caught access to prototype...
this.#spvt = HHGTTG

At no point do you ever have a chance to actually trap the access to the private field. You can interfere with it, but that interference would have also prevented the inherited call to run() since there is no way to tell why the prototype was being accessed. As such, you either interfere with all property access or you don't interfere at all.

The fact that I used a simple observer-type proxy instead of a membrane is of no bearing in this case. Use of a similarly functional full membrane would produce precisely the same result. As such, accesses to the prototype pose no threat whatsoever leading to the revelation of the existence of a private member.


Is there any counter argument? If not, how do we go about rectifying this mistake?

An alternative would be, at creation time of class B, installing the static private field directly on A (like how after super(), private instance fields are installed directly on this). That way, no prototype lookup would be required.

I don't recall why we didn't go with that solution.

I can offer my 2 cents. That solution makes no sense at all. Since private fields are installed at the time of construction, and are only installed on objects that pass through the corresponding constructor, there's no reasonable way to follow your suggestion because each constructor is a separately declared function.

From a different perspective, unlike class instances, constructor functions are individual objects linked together. That's both the source of the problem, and the reason why your suggestion doesn't work. There's no constructor to pass B through. Likewise, there is nothing that A is an instance of that can be inherited to make B both an instance of that derived something and whatever A would have been an instance of. TL;DR, the suggestion you're offering doesn't make sense in the context of ES.

There doesn't need to be one. When class B extends A is created - right before the class block exits, A's declared static private fields, and their current values, could be copied down to B.

1 Like

Took a minute to think about it. There's still a problem. The two values could diverge. The only way it makes sense is if the private field is copied to B as an accessor.

That's not a problem, that's already how they work.

let x = 0;
class C {
#y = x++;
}

In that example, every instance of C has a different value on #y. There shouldn't be any expectation that values that aren't constant remain constant.

I get what you're saying, but you're missing something fundamental. There's only ever one instance of a given class's constructor. As such, there should only ever be 1 set of static private members.

That being said, thinking about multiple descendants, I think I like your idea better. Given that every descendant inheriting static private members is a distinct instance all its own and may be expecting the values to have mutated in different ways, it makes sense to make strict copies.

@rdkng I'm not sure what you are ranting about. Sure, private class fields don't work they way you expected them to work. But can you give a convincing argument why they should have worked differently? I fail to see reasonable use cases for

  • using the same name for different things, on instances and class objects.
    Why do I say "different things"? Because no code should handle instances and their class object the same, for the same purpose.
  • inheriting private fields from one object to any other objects.
    You only mention static fields, but walking the prototype chain would also mean allowing Object.create(new Something).method() to access private fields of Something on the empty object. Very confusing, and with no proper way to distinguish "own" from "inherited" private fields.

If you want normal property access semantics, just use normal properties. With symbols as keys if you need to avoid collisions.

For the second point; no it wouldn't. Object.create doesn't install any fields, only the constructor and extends can do that.

I have to think more about it, but I believe this would make static fields more natural since derived classes are different object than their parent class, but do inherit their behavior.

However what's less clear is how the initialization of the copied fields should work:

  • if re-executing the initializer statement, it gives an opportunity to a class to detect when it gets derived
  • if copying the initial value, it would prevent the collection of that value even if the field changes later
  • copying the current value would introduce a weird snapshoting mechanism
2 Likes

Can I offer convincing argument? I don't know. I can only offer accurate and logical argument. Whether or not the argument is convincing is up to the receiver, not me. As for class fields not working the way I expected them to work, if it were only me, even I would dismiss my arguments. However, I am not alone in this opinion of how private fields (and even public fields) turned out. It's just that I am doggedly persistent about things that are unnecessarily illogical.

There are many such flaws in the implementation of private fields, and I have already gone on record many times vocalizing those issues, all of which were prioritized as being of low value to the members of TC39, not because of functional or logical deficiency, but rather for political reasons related to the desire to account for some feature that at best is only loosely related to the feature being developed. That led to a series of compromises that ultimately compromised the usability of the primary feature.

Whether or not you agree with this assessment of mine is of little merit as what is done is done, and all any of us can do now is make the best of the situation that is. My goal in this thread is to hopefully convince some members of TC39 through logical argument that certain portions of this proposal can be non-destructively altered to produce a more useful effect. As for your questions:

Sadly, this only points out a limitation in your understanding. A function is just as much an object as the instances it may produce by calling it with new. As such, either can be passed into a function expecting to receive an object.

Beyond this, you also made an invalid assumption. Just because I wish to have a member with the same name on both the constructor and an instance produced by it does not imply that the code using that property when passed either the instance or constructor treats the object it received in the same way. It is only true in this case because the property in question serves exactly the same purpose. I am merely asserting that since the constructor is a separate object from the instances it produces, there should be no naming conflicts between the members of the constructor and the members of an instance it produces. To do so violates a fundamental expectation of ES, and is therefore "surprising".

I'd like to see how you've come to this conclusion. My assertion is simply that in accordance with the fundamental design of ES, methods of Something when called from an object that inherits Something as a prototype, should not fail to find any member of Something that cannot be removed from Something. Failure to do so is, once again, a "surprising" violation.

As I said before. I can only offer logical arguments. Whether they're convincing or not is at the discretion of the ones that read them.

This opportunity has always existed. If the static initialization of a derived class requires the update of a member of the base class, the base class can easily be structured to perform some action on this event. I can't say for certain if this has been used before in ES code, but I know for certain it is used in some scenarios in other languages that support class.

I would prefer to see complete re-initialization if possible. This would result in copying all data members to the derived class, as would be normal for any other such object. The difficulty is in accessors. I can forsee a potential footgun where someone designs a static accessor that uses the class constructor itself as the key for some storage instead of this, causing any newly created derived class to clobber existing data. I would consider this risk relatively minor by comparison, certainly less likely than someone clobbering inheritance via a conflict between accessors and fields.

This is the main reason why I would prefer to see complete re-initialization. After a thought or two, copying the data from the base class would be a footgun no different than the one caused by putting a mutable object or array on a prototype. Wasn't this one of the main reasons why public fields were applied to the instance instead of the prototype? Nevermind that if they had been put on the prototype, there would have been no "define vs set" argument, the initializer function approach could have been kept, and the resulting footguns (object on prototype, accessor vs field) completely avoided. TL;DR simply copying the data can lead down a rabbit hole that hold nothing but bad decisions.

What I mean is that currently a class cannot automatically sense the point where it's derived. A derived class can always reveal itself when calling something on the base class. Re-initializing the static private fields on the new object would allow the base class to know exactly when it's derived.

Put another way, there is currently no concept of static constructor, only static init blocks, and this would in effect implicitly define a static construction mechanism.