Allow `this` in static methods to access any members without exception

Problem

From MDN Web Docs:

There is a restriction on private static fields: only the class which defines the private static field can access the field. This can lead to unexpected behavior when using this. In the following example, this refers to the Subclass class (not the ClassWithPrivateStaticField class) when we try to call Subclass.publicStaticMethod(), and so causes a TypeError.

class ClassWithPrivateStaticField {
  static #privateStaticField = 42;

  static publicStaticMethod() {
    return this.#privateStaticField;
  }
}

class Subclass extends ClassWithPrivateStaticField {}

Subclass.publicStaticMethod();
// TypeError: Cannot read private member #privateStaticField from an
// object whose class did not declare it

[…]

You are advised to always access private static fields through the class name, not through this, so inheritance doesn’t break the method.

So this cannot be used in static methods to access private static members (the example for fields above also applies to methods) because it breaks for subclasses.

However this can be used in instance methods to access private instance members (the example for fields below also applies to methods) because it works for subclass instances:

class ClassWithPrivateInstanceField {
  #privateInstanceField = 42;

  publicInstanceMethod() {
    return this.#privateInstanceField;
  }
}

class Subclass extends ClassWithPrivateInstanceField {}

new Subclass().publicInstanceMethod(); // 42

The fact that this can be used in static methods to access any members, except private static members, has two drawbacks:

  1. Information duplication by requiring to use the class name for the exception to the rule.
  2. Bad developer experience by requiring to memorize the exception to the rule.

Solution

Lift the restriction that forbids access to private static members through subclasses.

Prior art

Python doesn’t have that restriction, so this can be used in static methods to access any members, including private static members (prefixed with __):

class ClassWithPrivateStaticField:
    __private_static_field = 42

    @classmethod
    def public_static_method(this):
        return this.__private_static_field


class Subclass(ClassWithPrivateStaticField):
    pass


Subclass.public_static_method()  // returns 42

Python doesn't have private properties per se (it merely has the underscore convention), so the example is inadequate as you can do the same in JS as well. C++/C#/Java doesn't allow using this in static methods and none of them allow accessing Subclass.privateStaticFieldFromSuper either. Not sure about others.

This restriction was explicitly decided upon due to the conflicting mental models of how inheritance should work for them, and is unlikely to ever be lifted. I can't locate the issue or the notes where it was discussed, but hopefully someone can reply with the relevant links.

1 Like

There's also that afaik no other language has truly private fields, since "accessible via reflection" is still public, so I'm not sure if there's truly equivalent prior art to look at.

See also cross-post at javascript - Why is access to private static members through a subclass forbidden? - Stack Overflow

There was this issue in the proposal repo:

2 Likes

Hmm, I don't think it's fair to discount languages that allow reflecting private fields. Yes, it does mean their implementation will be a little different, but unless those differences actually affected this specific decision, it still makes sense to consider what they were doing, why, and how it worked out for them.

I'm struggling to understand the arguments in here. It feels like the meat of the argument (in the one answer given) is that instance fields are created when the prototype relationship is already known while static fields can be created before anyone ever actually inherits from them.

But, I fail to see why that matters.

TBH - I never even realized you could use "this" inside of a static function - it makes sense after thinking about it for two seconds, it just never ocurred to me to do it, I've always used the class name. I'll have to start using "this" in static functions now... unless I'm accessing a private field and I expect inheritance relationships to be built from the class.

Note that I was not referring to the single underscore prefix (_) which indeed restricts access to private members only by convention (no language-based access restriction).

I was referring to the double underscore prefix (__) which restricts access to private members by name mangling (language-based direct access restriction): a member __m of a class C is mangled to _C__m which prevents the direct access this.__m (but the indirect access this._C__m is still possible).

So both strategies are limited forms of access restriction, but the latter is language-based like the hash prefix # in JavaScript so they are similar in that regard (though access restriction in JavaScript is stronger).

GitHub - tc39/proposal-class-access-expressions: ECMAScript class access expressions feels like a nice solution to this issue.

this.#foo becomes class.#foo

1 Like

It seems conflicting only for implementers, but for mere users like me it’s the current behaviour which is unexpected. I expect the member accesses C.m (or this.m) and C.#m (or this.#m) to walk the prototype chain for any static members; I don’t expect a special restriction for private static members.

The privacy of a member should only restrict the scopes from which it can be accessed (i.e. inherited), not restrict the access (i.e. inheritance) completely: m is public so C.m (or this.m) is allowed in any scope, whereas #m is private so C.#m (or this.#m) is allowed only in the scope of the class that defines #m. That’s what Python subtly does as shown in the original post and what JavaScript currently doesn’t.

That does seems like a more confusing behaviour to me as a user. Why should static members be treated differently than instance members? No private member access walks the prototype chain, that's a simple rule to memorise. There is no extra "restriction" as you call it, this is just the normal behaviour.

I would be cautious with that. Especially if you don't expect other classes to inherit from yours, you should generally not use this but always refer to your class directly by name. Only if you expect derived classes, and if you want them to be able override the methods you are calling, you should use this.

class would indeed be a nice addition to the language (Python also has it, it’s named __class__). But class can’t replace this in general because this allows the member to be overridden in a subclass whereas class doesn’t, so the use case is slightly different. Static private members can’t be overridden so in that case both class and this should be equivalent (in Python they are equivalent in that case), but that’s not what currently happens since this doesn’t work in that case. So class is a different feature that happens to coincide with this for static private members, but that doesn’t mean this shouldn’t be fixed. Users just want to be able to use this uniformly (i.e. for any members: public or private).

That’s exactly that rule that I’m questioning in this thread. Privacy should only prevent inheritance (i.e. member access) outside of the scope of the class defining the member; it should not prevent inheritance completely (i.e. including in the scope of the class defining the member).

Hm, but wouldn't that be confusing as well?

class Example {
   #priv = 1;
   static {
      const e = new Example();
      console.log(e.#priv); // normal, works as expected
      const clone = Object.create(e);
      console.log(clone.#priv); // what should this do?
      clone.#priv = 2; // should this work as well?
      ({}).#priv = 3; // and if yes, what about this?
   }
}

(using private members of an Example instance here, but it's the same for private static members)

I mean, I would really have loved if private names were just privately scoped and would otherwise work like any other property, on arbitrary objects (raising questions about bracket syntax and proxies though), but that's not what the committee decided on. While I don't like it either, it's at least consistent. Whether it's too late to change without breaking the web, I cannot assess.

The primary difference here is that private instance fields are all installed at instantiation time - iow, either when a class is constructed, or when something extended it is constructed and invokes it via super(). The inheritance chain is already set up - and whatever the chain is at new time is how private fields become installed.

A private static field, however, is installed at the time the class is created - which is before any subclasses have had a chance to extend it. There's either no code there to invoke, and so no way to install it on a subclass at extension time - or, there is code there to invoke, and an object value will be duplicated - or, the value is assigned from the superclass.

In the former case, you have a hazard/bug farm (that already exists for objects stored on the prototype, but that's not easy to do with class syntax, by design). In the latter case, you have a surprising inconsistency with instance fields, which are re-evaluated every time.

If we were to re-evaluate a static instance field, then you'd be able to observe/hook into "extension", which is not a current capability nor one that has consensus to exist.

The issue doesn't occur for public static fields because public fields, unlike private fields, DO walk up the prototype chain.

Hopefully that clears it up a bit.

1 Like

Yeah - I mostly was talking about internal only classes, i.e. "I don't expect others to inherit" == "this class is only used in my own code and is not publicly exported, so I don't have to worry about random users inheriting it".

Then again, maybe I won't do it. It could be confusing following a double standard like that, as opposed to just always using the class names.