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.

Why would that even be a goal? The way I see it, a sibling class originates from the same source code, and I believe protected to be source-positional (at least in my mind, and that's simpler).

@jridgewell Regarding your linked proposal above,

private #createPart;

class AttributeCommitter {
  //...

  [#createPart]() {
    return new AttributePart(this);
  }
}

class PropertyCommitter extends AttributeCommitter {
  [#createPart]() {
    return new PropertyPart(this);
  }
}

How do we do that across separate modules? I wouldn't want to put all my classes in the same file just to share protected state.

I’ve not yet seen any way to achieve that (cross-scope sharing, that remains open for the lifetime of the program to new trusted code, but that isn’t open to any untrusted code), and without that, i don’t believe “protected” is a concept that belongs in javascript.

There’s only “reachable” and “unreachable” - it’s not an “access level” with more than two options.

1 Like

Sure you have. I showed how it was possible to you. I later crafted a library that does precisely that, and I've been using that approach with each successive library. It is definitely possible to do. The trick is that the trusted code is all in the library that implements private and protected. Because the code is in 1 place, and that code knows about all the classes that have been created using it, it is able to manage the protection scheme.

If you want, I can give you a copy of the original library to play with. My latest library is still under development at GitHub - rdking/ClassicJS: Adds full private & protected support over ES6 classes without blocking Proxy., but it should be fairly stable.

Simple. If class C extends A can leak properties from class B extends A common to class A then protected would be no different than public. On this point, I whole-heartedly agree with @ljharb. The way I see it, a sibling class is any class that extends the same base as some other class. It doesn't matter if all 3 classes involved are in different source files or even from different authors.

In my opinion protected is about encouraging best practices and design patterns. In Java, all protected and private members are acceptable by Reflection, yet those features do successfully encourage best practices.

From the concepts that I've seen (including my own implementation in lowclass), reaching the protected members from the outside involves writing convoluted hard-to-understand code. For example.

This issue in my implementation can be solved by simply making instance prototypes non-configurable.

In a native implementation, no such issue would ever arise, because the protected inheritance can be totally hidden.

My implementation is in JavaScript, where I can't hide protected and private prototypes. I am 100% sure that if I implemented this at the engine level, I'd be able to 100% hide protected members from public access. My first attempt would be using call-stack tracking, among other things, which is literally impossible in JavaScript with async call stacks, but not impossible at the engine level.

If I can can imagine how to achieve protected access with synchronous call stack tracking in plain JavaScript (@rdking knows about call stack tracking too I believe), then I know you can too! I didn't try it because I knew it wouldn't work as soon as async code broke out of the synchronous context.

It is possible to implement async call-stack tracking with generator functions, because all points of exit from and entry to async processes are controlled by a scheduler that knows when to return to a yield, and therefore knows which part of a stack it is returning to. But the user would have to explicitly use our own generator implementation instead of native async/await.

We could compile async/await with Babel to use our own implementation of generators, and then we'd be able to track call stacks in async code, to make this proof of concept.

Once we're tracking call stacks, we have information that could be used in a protected implementation.

If I can visualize it, I can make it! :)

@rdking said

Because the code is in 1 place, and that code knows about all the classes that have been created using it, it is able to manage the protection scheme.

Exactly, and this is precisely what the native engine is in relation to all JS code that the engine runs.

How can C leak properties from B? From perspective of C, I would treat a property access on a B instance as public because the inheritance tree path diverged and does not match.

Possible ways to make the rules:

  1. Using "assignability" could be one way to make the rules: C can access B protected if B is assignable to C, but B isn't assignable to C. Similarly, if D extends C, then C can access D protected because D is assignable to C (D is a C), but D can not access C protected because C is not assignable to D (C is not a D).

  2. Another way to make the rules could be that the hierarchy of one class must be a subset of the other class, and either class can access their protected members if and only if the protected member is inherited from a third class hierarchy that is a subset of the two classes accessing each other's protected members. This is slightly less restrictive than the previous assignability-based version.

I prefer option 2.

I think it may be worth making future concepts using a fork of a browser and implementing the concepts in native code (unless the concept is 100% implementable in pure JS, as not all concepts are). Making concepts in pure JS may be limiting potentially good outcomes for the JS community.

@ljharb What if TC39 required Stage 2 proposals to have an implementation based on a fork of a browser using native code, before being allowed to enter Stage 3 (unless the concept is 100% implementably in pure JavaScript)? This might be a really good thing! It'd at least help everyone verify concepts in an environment with the true flexibility that can't be polyfilled in JavaScript, and therefore would lend to better discussion on the benefits and merits of the concepts before they get to Stage 3 and 4.


@rdking I wish you were on TC39 with the time you need to implement your concepts at the native level.

For that matter, I wish I were too, as then I too would probably have more time to prove my theories while getting paid. At the moment, I don't have this opportunity, and I have only limited unpaid free time for this sort of stuff. I'm assuming that TC39 members often get to work on these things while they are paid by the major companies that employ them.

The initial proposal had a export { foo } for './some/file.js' which restricted the ability to import foo to only the (relatively resolved) ./some/file.js module. But, this is a separate concern than private declarations.

This is the same as Symbol properties. Feedback we received from library authors was that this wasn't private enough, and people will still write code that depends on your "private" properties.

We've already discussed this in committee, and rejected it. It would make access speeds unacceptably slow compared to public properties.

Has an implementation been tried and measured yet?

Implementation idea:

// get the current function, and see
function canAccessProperty
  if (homeObjectMethods.has(stack[stack.length - 1])
    return true
  return false

where homeObjectMethods is a list (Set) of methods defined on the prototype of the class ([[HomeObject]] of the methods) where the protected member is defined. This would be looked up during prototype lookup in a [[Get]] or [[Set]] process, in the same (or similar) way that other intrinsics like Object.preventExtensions are looked up.

Private fields in JS are inaccessible even by that, and they aren't exposed to subclasses, either. Transpilers use weak maps invisible to user code, and engines use internal fields not unlike what they use for built-in types.

Some of my libraries do use call stack tracking to verify permission. This works whether synchronous or async since the call chain to get the private and protected members is always synchronous. This should work for generators as well, but I'm not certain. I haven't tested that. The funny part is that I only do this because ES provides no means of directly associating a method with the constructor function of its class. If it did, or if my designs were in-engine, then stack tracking would be pointless.

You have to be careful about this. Data privacy for "protected" is tricky. Think of it this way. It's perfectly fine for me to know my brother's secrets. We live in the same house so hiding things is impossible if he leaves them in the house. However, it's completely weird for me to know about my cousin's secrets just because we have the same grandfather since we don't both live with him. This is an analogy of what I expect from protected.

Of the 2 rules you gave:

  1. Fails because my dad can see what I only share with my kids. I think you got your C's and D's backward. A child can see what the parent is sharing, but the parent can never see what the child is hiding.
  2. Fails because it allows cousins to peek in on each other when they come to visit.

Protected is a 1-way (parent to child) inheritance relationship.

The major difference between Java and Javascript is that in Java, once written, a class is concrete. In Javascript, a class and it's instances are freely modifiable. ES is not a class-based language like Java, so certain so-called "best practices" in one language simply won't translate well to the other. In a way, I agree with @ljharb in that these discussions are only about whether or not something is "reachable". Where he and I disagree is that I give high importance to the clause "from where".

Is this while not in "strict mode"? Well, anyways, let's not get int the details, because there's no issue if we do it in the native engine.

Leaving things in the house is leaving things in public space. If we want to have something like "module protected", that'd be different, and we'd have to provide a mechanism for that.

I give high importance to the clause "from where".

So do I, and I believe in the rules I set because they're simpler and easier to understand. If you don't like them, well, that's a valid opinion!

I suppose we'd need to settle on where we want visibility.

Would it be okay to add static analysis to the JS engine? If so, then we could take my simpler rules, and add features for configuring visibility across modules, like:

class Foo {
  @visibleIn('./OtherClass.js') // module identifier
  protected someMethod() {/*...*/}
}

or

class Foo {
  // make it accessible within source code of a specific class or function by export name
  @visibleIn('./OtherClass.js').OtherClass
  @visibleIn('./OtherClass.js').someFunction
  protected someMethod() {/*...*/}
}

or depending on decorator syntax, maybe it'd beed to be

class Foo {
  // make it accessible within source code of a specific class or function by export name
  @visibleIn('./OtherClass.js', 'OtherClass').
  @visibleIn('./OtherClass.js', 'someFunction')
  protected someMethod() {/*...*/}
}

This would be what you're talking about: one person deciding what to share with others.

We could change my rules so that inherited members are visible in any one who inherits them, but maybe we don't have to if we have more flexible mechanisms like that previous example.


How can we make JS classes better than in all other languages? (IMO, class fields aren't in the be-better-than-all-languages direction, at least not yet, which is obvious when we consider how much the proposal-class-fields FAQ aims to satisfy expectations of other languages.)

A house is private to the family that owns it for the sake of this kind of image. So while things in the house are "public" to the family members, those same things are "private" to non-family members.

The rules for "protected" are a little more nuanced than that. Given a base class "A" with protected property "p", and derived classes "B" & "C", the instances of both derived classes will have access to an "effectively private" member "p". However, "C" should not be able to access "B::p", and likewise for "B" & "C::p". On the other hand, "A" can access both "B::p" & "C::p" since it is a sourcing ancestor.

Getting these rules wrong will certainly lead to surprising results, especially for developers used to class-based languages.

True, but that sounds more like a "module protected" feature (something like "package protected" in Java, but "module protected" is within a file instead of a package).

Why is that the rule? Is that your preference? Why do you prefer that?

The rules can be whatever we eventually choose. Maybe there could be different forms of protected (that'd be cool I think, but not sure how the syntax would accommodate it). F.e. one form allows any subclass that inherits the protected member to access the member of any other class as along as the other classes also inherit the same base, and the other form(s) could be more restricted options.

For example, my previous option 2 fits your case well:

In your example, C extends A and B extends A, then the hierarchy of C is not a subset of B, or vice versa, therefore C can't access B.p, and same with B and C.p.

Any of the rules we've discussed are implementable. A more important thing may be to get a native implementation working (with any rules resembling protected) and then go from there.