Secretly setting JavaScript private class fields

March 12th, 2024


You can use Symbols to set private class fields.

var A = class {
    #b
    constructor(s) {
        this[s] = function(v) { this.#b = v }
    }
    get b() { return this.#b }
}

s = Symbol()

a = new A(s)
Reflect.apply(Reflect.get(a, s), a, [1])
a.B // returns 1

I use private class members for storing reducer state primarily because they disallow bracket accessor notation a['b'], so you can’t have dynamic access to other reducer state. That allows me to statically analyze the reducer and make sure that the reducer dependencies are not cyclical.

In addition, the static methods are user-generated and I did not want them setting other private field values. I can guard against explicit assignment on the private field through static analysis, but if I had left a Stream.set method, I had to guard against that as well, which is tricky because it would be accessible via bracket accessor notation.

In addition, the use of Reflect methods is largely to make it easier to instrument from the v8go library, but the Reflect library allows for some undesirable metaprogramming from the user. Reflect.ownKeys, for instance, would need to be guarded against.

class Stream {
    #reducerName
    static reducerName(state, event) { return state }

    // This code is generated by the system and hidden from the user
    constructor(sym, { reducerName }) {
        function deepFreeze(object) {
            if (object && typeof object === "object") {
                const propNames = Object.getOwnPropertyNames(object);
                for (const name of propNames) {
                    const value = object[name];
                    if (value && typeof value === "object") {
                        deepFreeze(value);
                    }
                }
                return Object.freeze(object);
            }
            return object;
        }

        this.#reducerName = deepFreeze(reducerName)

        this[sym] = function(reducerName, state) {
            switch (reducerName) {
            case 'reducerName':
                this.#reducerName = state
            default:
                throw new Error('unknown reducer')
            }
        }
    }
}