Protected support for `class`

For better or worse, class-fields is pretty much a given. However, it doesn't provide any support for protected access. One of the reasons why is due to the risk of loose scoping that would allow an inheriting class' methods to leak the protected data of a sibling class instance. This problem can be completely mitigated with Symbol-keyed, out-of-line accessors. The idea is that for any pair of base class and subclass, protected field access will follow this convention:

SubClass.#field -> SubClass.[[ProtectedFields]][fieldSymbol] -> BaseClass.[[ProtectedFields]].field

where [[ProtectedFields]] is an instance-bound object attached to each class instance, and containing protected fields/methods/accessors for the owning class and only accessors for the subclasses. Since leaking would be possible if SubClass instances had direct access to protected members of BaseClass, subclass instances of bases with protected data will contain private accessors that reference a symbol-keyed accessors in their [[ProtectedFields]], which in turn will reference the base class' field.
Since the subclass will use a class-unique symbol to refer to a protected field, it will be impossible for a sibling of the subclass to leak the protected data of the subclass.

An example of the logic to perform this using ES6 code exists here.

This generalizes into creating a wrapping private accessor for protected data on every subclass, right?

class Base {
  protected #data = 0;
}

class Sub extends Base {
  // Internally
  get #data() {
    return this[BaseDataPrivate];
  }
  set #data(v) {
    this[BaseDataPrivate] = v;
  }
}

That will require another property on every subclass instance per protected property, which is an issue. But it also means that adding a protected field to the base class is not an isolated change, any subclass could break because it now the field is reserved by the base class.

Have you looked at https://github.com/tc39/proposal-private-declarations yet? It accomplishes protected in an explicit way, and doesn't require extra properties to get around subclasses' access restrictions.

That's about right.

Why? I don't see the issue. Can you elaborate?

Incorrect. If a subclass declares it's own private field with the same name as that of a protected field in the base class, then there will be no connection between the subclass and the base class for that field. Put another way, a subclass can always override a parent class' protected field.

I just did. There are several issues with that approach:

  1. it requires private names to be publicly instantiable. That reduces them to little more than non-enumerable symbols... i.e. Private Symbols, which is a rejected proposal.
  2. It will add more weight to the confusion of whether or not # is a valid name character.
  3. It will raise more arguments as to why private #field can't be used inside a class when we're using it outside, essentially increasing the difficulty of the mental model.
  4. It doesn't fit with ES's current model for object/primitive creation. (You're creating a value with a declaration that makes it look like a container.)

I could go on with more ways that proposal-private-declarations is a bad idea if you like, but the biggest issue is

  1. There's no private means of sharing the protected name when 3rd party developers want to extend your classes.

The approach I've given doesn't have that problem. Besides, the proliferation of accessors can be internally reduced to a set of alias names that all reference the same private field, each subclass adding its own alias. Using accessors is just an approach to produce the required logic using ES code.

Regular private methods/accessors require installing a property (let's call it a word) on every instance, which is different than public methods that are installed once on the prototype. This extra word on every instance is already worrying for implementations (it's a memory issue that's never existed before). Having protected properties require 2 words every subclass instance is even more worrying.

That's better than I initially thought, then.

Private symbols are a declaration, not a value. They don't evaluate to anything, and you can't use them in any way other than defining private fields on classes.

By design. If 3rd party developers can have access to your private properties, they were never private to begin with. A symbol property would have been the correct use here.

That's one of several reasons why I'm staunchly against the whole field concept. I get why it's being pushed, but it's an approach that doesn't quite fit ES's existing paradigms. This is just one of the issues, and a relatively minor one by comparison.

Let's compare:

  • let x is a declaration. Evaluates to the value of x.
  • function foo() {} is a declaration. Evaluates to the function foo.
  • class Ex {} is a declaration. Evaluates to the constructor function Ex.

On the other hand:

  • private #fieldName is a declaration. Evaluates to nothing?

I would understand if it evaluates to a useless PrivateName object with completely inaccessible parts. But to have it not evaluate at all begs the question of why it's being publicly declared in the first place. If it's only useful within the scope of a class, then it should only exist within the scope of a class.

Your statement violates what imo is the most important use case for protected. Think of this: In C++, C#, and Java, isn't extending and overriding library-provided classes and methods a very large part of most code bases? What about UI libraries? Many custom controls can be created by composition, but there are also many controls that either cannot be developed without or cannot be properly used unless they're extended.

The main purpose of protected isn't to share data between otherwise unrelated classes, but rather to provide base classes with a means of providing a subclass-only API that allows the subclass to manipulate the functionality of the base class in ways not provided to non-subclasses. This "shared exclusivity" is the core feature that people asking for protected are expecting. That proposal you showed me managed some sharing and some exclusion, but failed completely at melding the two.

Essentially. It's because you cannot use it as an expression anywhere but in defining class fields. Eg, foo(#fieldName) is a syntax error, as is #fieldName.bar, etc. It is purely a declaration and has no associated "value".

I understand, but I think that this is already addressed by using public Symbols.

I get what you're after. I still have to say that this goes against ES paradigms. Everything is either a primitive or an object in ES. This will be the first time that there is something that needs to be declared but is essentially nothing. ES doesn't have this concept.

Within ES paradigms, any [[IdentifierName]] should be an RVALUE. A declaration like private #foo should declare a constant RVALUE named #foo that can be assigned to variables and passed around as a parameter to functions. To use this declaration like this.#foo, you're not actually declaring an object or primitive, but rather a new piece of syntax. That's a very weird thing in ES. I don't see how it has a place. You might as well go back to pushing for private symbols. They'd serve the same purpose without introducing such an oddity in the language.

If the need for protected was satisfied by public Symbols, who would be pushing for protected? We'd already have it. For requirements, protected members must be:

  • Hard private within the declaring class
  • Hard private within each derived class
  • Protected members of a derived class must not be accessible to methods of any class not a direct ancestor of the derived class.
  • Protected members of a declaring class must not be accessible to methods of any siblings or ancestors of the declaring class.
  • Derived classes must be able to override protected members. An overriding class is a declaring class.
  • Non-overridden protected fields must be accessible by the methods and accessors of every ancestor back to the declaring class.
  • Protected members must be inheritable between base- and sub- classes created in separate modules.

Public Symbols cannot be used to fill all of these requirements. The truth is that a proper implementation of protected is one of the more complex things to accomplish in ES without modifying the mental model. The choices regarding both the implementation of class and the implementation of private fields only add to this complexity. A little extra memory in the form of a copied property descriptor is a comparatively small price to pay for flexibility gains provided by this feature.

The implementation could work by providing a private field in the base class and a single accessor property descriptor attached to each of the corresponding private fields in the derived class. In this way, there would be only 1 accessor used by all subclasses, but each subclass would reference the accessor by a different private name.

Here's a thought regarding the memory issue of multiple accessor properties:

The way accessors are being used in my suggestion is to make aliases of the protected member so that derived siblings cannot access each other's protected members. If it was possible to make multiple aliases in name only to reference a single member element, then the memory issue would be solved. To that end, since a PrivateName is a key to an internal slot, if it were possible to make it so that multiple PrivateNames could reference the same internal slot on an instance, then that should satisfy the same need as having multiple getters.

I've been working on an ES6 implementation of this idea where the protected field is moved into a protected container and accessed via an accessor even by member functions of the declaring class. All derived class methods use the exact same accessor descriptor to declare their accessors for that protected member. I'm doing this in the hope that engines will use the same function references for get and set on each alias. That should reduce the memory load significantly for deep trees.