1

I'm playing around with ES6 classes in JavaScript and I'm wondering if it's possible for a child class to inherit private properties/methods from a superclass, while allowing the subclass to mutate this private property without using any "set" methods (to keep it read-only).

For example, say I want to create a static private property called #className in my superclass that is read-only, meaning you can only read from it using a method called getClassName() and you cannot access this property by doing class.className. Now, I make a new child class that extends this superclass. The child class will inherit #className and getClassName(), but, I would like #className to be initialized with a different value in the child class than in the superclass. So, in the superclass you could have: #className = 'Parent' but in the child class you would have #className = 'Child'.

My only problem is, it seems like you can't really do this. If I try declaring a new #className in the child class, the child class's getClassName() method still refers to the #className from the superclass. Here's the code I was playing with:

class Parent {
    #className = 'Parent' // create private className

    constructor() {}

    getClassName() {
        return this.#className; // return className from object
    }
}

class Child extends Parent {
    #className = 'Child' // re-define className for this child class

    constructor() { super(); } // inherit from Parent class
}

new Child().getClassName() // --> this prints 'Parent' when I want it to print 'Child'

Does anyone have a solution to this? Or an alternative that achieves a similar affect?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Classes cannot see each other's private parts. Each class declares its *own* `#className` private field. If you expect `getClassName()` to return a different value for the child instance, override the `getClassName` method. – Bergi May 31 '22 at 01:49
  • 1
    @Bergi Is that the only way? If I have to override the inherited methods with code that's just doing the same thing, I feel like that kind of ruins the point of using classes and inheritence – WillWillington May 31 '22 at 01:54
  • 1
    If you want to make the class extensible by allowing to subclass it and override the classname, just don't make it private! It should be a normal public property like any other. (Or, at best, marked in the documentation as "protected", i.e. only subclasses should assign values to it) – Bergi May 31 '22 at 03:11
  • Can you provide a more realistic example of what you actually want to do? Because in your demo, the entire thing should be simplified to `get className() { return 'Parent'; }` which could easily be overridden by `get className() { return 'Child'; }`. – Bergi May 31 '22 at 03:15
  • @Bergi Well, the main reason I wanted to solve this specific problem was to prevent cases where you can change the fields directly in the class itself, such as doing `class.field = ...` instead of using the class methods to do `class.change(field, ...)`. There might be additional functionality in the class methods that should always execute when a property is changed, which would be bypassed if a user just did `class.field = ...`. I guess an example could be, for instance, if you want to make a counter that counts how many times you've changed a property – WillWillington May 31 '22 at 03:24
  • 1
    That's what getters and setters were invented for. – Bergi May 31 '22 at 20:11
  • Yeah, I'm basically trying to make it so that whoever is using the class can _only_ interact with the data through getters and setters, and not directly by indexing the object for the property. Hopefully I'm making some kind of sense here, my apologies I'm still fairly new to OOP. – WillWillington May 31 '22 at 20:17
  • 1
    A subclass can easily extend the behavior of the getters & setters. Of course, if the parent class setter writes into a private field and counts that (`set field(value) { this.#field = value; this.count++; }`), a subclass can intercept and manipulate accesses by overriding the getter, but it cannot itself write into the private field directly without counting, it'll have to do `super.field = value`. It can only *add* functionality. – Bergi May 31 '22 at 20:21
  • 1
    @Bergi Ahhh okay, I think I get it now. I think the original idea behind my question was just flawed then. Thank you very much for your time, your replies have been very helpful! – WillWillington Jun 02 '22 at 03:41

1 Answers1

2

JavaScript does not support directly accessing private properties inherited from another class, which is how private members are supposed to work. You seem to want the functionality of protected properties. As of 2022, JavaScript does not support protected properties or members of any kind. Why that is, I can't imagine, since other OOP languages have allowed said functionality since time immemorial.

If you have control over the code of the parent class, you can simulate protected properties by using symbols.

const className = Symbol();

class Parent {
    [className] = 'Parent'; // create protected [className]

    getClassName() {
        return this[className]; // return [className] from object
    }
}

class Child extends Parent {
    [className] = 'Child'; // re-define [className] for this child class
}

console.log(new Child().getClassName()); // --> this prints 'Child'

I'm not sure why this snippet fails in the preview even with Babel. That exact code appears works in the console of every major browser I've tried.

The reason this works is that a Symbol in JavaScript is a primitive type that's guaranteed to be unique. Unlike other primitives, when used as a key in an object, it cannot be [easily] accessed or iterated over, effectively making it protected.

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol

Whether a property is truly "protected" is primarily determined by the scope within which you define the symbol (or whether you pass it around).

For instance, if the above code is module-scoped then that symbol will only be accessible to anything that's within that scope. As with any variable in JavaScript, you can scope a symbol within a function definition, or an if-statement, or any block. It's up to you.

There are a few cases where those symbols can be accessed. This includes the use of Object.getOwnPropertySymbols, Object.getOwnPropertyDescriptors, to name a few. Thus, it's not a perfect system, but it's a step above the old-school way of creating "private" members in JavaScript by prefixing names with an underscore.

In my own work, I use this technique to avoid using private class member syntax because of the gotcha you describe. But that's because I have never cared about preventing that capability in subclasses. The only drawback to using symbols is it requires a bit more code. Since a symbol is still a value that can be passed around, that makes it possible to create "loopholes" in your code for testing purposes or the occasional edge-case.

To truly eliminate leakage of conceptually protected properties, a WeakMap within the scope of the class definitions can be used.

const protectedClassNames = new WeakMap();

class Parent {
    constructor() {
        protectedClassNames.set(this, 'Parent');
    }

    getClassName() {
        return protectedClassNames.get(this);
    }
}

class Child extends Parent {
    constructor() {
      super();

      protectedClassNames.set(this, 'Child'); // re-define className for this child class
    }
}

console.log(new Child().getClassName()); // --> this prints 'Child'

A WeakMap is a key-value store that takes an object as a key and will dereference the object when said object has been garbage collected.

As long as the protectedClassNames WeakMap is only scoped to the class definitions that need it, then its values won't be possibly leaked elsewhere. However, the downside is that you run into a similar problem to the original issue if a class that's out of scope of the weak map tries to inherit from one of the classes that uses it.

WeakMaps aren't strictly necessary for this, but its function is important for managing memory.

Unfortunately, there appears to be no proposal in progress for adding protected members to the JavaScript standard. However, decorator syntax combined with either of the approaches described here may be a convenient way of implementing protected members for those willing to use a JavaScript transpiler.

Ten Bitcomb
  • 2,316
  • 1
  • 25
  • 39
  • 1
    "*As of 2022*" - it's unlikely that will ever change. "*Why that is, I can't imagine*" - because they're **private**. You're thinking of "protected" members? – Bergi Jun 18 '22 at 14:57
  • "*when used as a key in an object, [a symbol] cannot be accessed or iterated over, effectively making it a private entity*" - [wrong](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols) – Bergi Jun 18 '22 at 14:58
  • @Bergi Are you referring to Object.getOwnPropertySymbols or Object.getOwnPropertyDescriptors? Yes, they can be accessed that way, but that's borderline tampering depending on how one looks at it. Any code can have its private or protected members exposed given enough effort by the end developer. Under normal circumstances, a developer who does a for-in or Object.[keys|values|entries] won't stumble upon a symbol property. Even if they do, they won't necessarily know what to do with it. – Ten Bitcomb Jun 18 '22 at 15:40
  • 1
    Well if you count non-enumerable properties as "private" (or protected), JavaScript had those since ES5, you don't need symbols for that – Bergi Jun 19 '22 at 04:16