Search code examples
javascriptclassinheritanceprivateclass-fields

Can a child class overwrite a private field inherited from a superclass?


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?


Solution

  • 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.