Search code examples
javascriptprivate-memberses6-proxy

Can a Proxy really provide access to private fields of a class?


I'm reading the documentation here, and it seems to imply that even for private fields, it would be possible to access them via a Proxy. See the blurb that starts with "....To fix this....". But the example given, doesn't work. My code is shown below:

The class with the private field:

class Danger {
    #areYouSure = "magic";

    grave(){
        console.log("Yes, grave danger");
        return true;
    }

}

module.exports = Danger;

Trying to use a proxy to access it:

const Danger = require('./testing-private');

const handler = {
    get(target, property, receiver){
        return target[property];
    }
};
const dangerProxy = new Proxy(new Danger(), handler);

console.log("--------------------------------------", dangerProxy.areYouSure);

This prints out:

-------------------------------------- undefined

If I replace "dangerProxy.areYouSure" with "dangerProxy.#areYouSure". I get

/home/zaphod/code/math/js-experiments/src/danger-proxy.js:29
console.log("--------------------------------------", dangerProxy.#areYouSure);
                                                                 ^

SyntaxError: Private field '#areYouSure' must be declared in an enclosing class
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1032:15)
    at Module._compile (node:internal/modules/cjs/loader:1067:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1155:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47

Is there a way to get this working or are my expectations incorrect?


Solution

  • The section of the documentation is really about this within a getter property (though it's not very clear about that), not about private properties per se. It's just that the value of this within the getter matters more when the getter is accessing a private field.

    Suppose we do the same thing using a pseudo-private property (private only by convention):

    class Secret {
        _secret;
        constructor(secret) {
            this._secret = secret;
        }
        get secret() {
            return this._secret.replace(/\d+/, "[REDACTED]");
        }
    }
    
    const aSecret = new Secret("123456");
    console.log(aSecret.secret); // [REDACTED]
    // Looks like a no-op forwarding...
    const proxy = new Proxy(aSecret, {});
    console.log(proxy.secret); // [REDACTED]

    As you can see, that works. But why does it work when a true private field didn't? Because of the value of this during the getter call. Let's see what this is:

    class Secret {
        _secret;
        constructor(secret) {
            this._secret = secret;
        }
        get secret() {
            console.log(`this === aSecret? ${this === aSecret}`);
            console.log(`this === proxy? ${this === proxy}`);
            return this._secret.replace(/\d+/, "[REDACTED]");
        }
    }
    
    const aSecret = new Secret("123456");
    const proxy = new Proxy(aSecret, {}); // (Had to move this, but it doesn't change anything important)
    console.log(aSecret.secret); // [REDACTED]
    console.log(proxy.secret); // [REDACTED]

    When we use aSecret directly, this is (perhaps obviously) aSecret:

    this === aSecret? true
    this === proxy? false
    [REDACTED]
    

    ...but when we go through the proxy, this is the proxy:

    this === aSecret? false
    this === proxy? true
    [REDACTED]
    

    That's because the default implementation of the get trap effectively looks like this:

    get(target, propName, receiver) {
        return Reflect.get(target, propName, receiver);
    }
    

    In proxy.secret, target will be aSecret, propName will be "secret", and receiver will be proxy. Reflect.get will call the getter function using receiver (the proxy) as this, because that's what the third argument is for, the value of this during the operation. this being proxy instead of aSecret during the operation doesn't matter for our pseudo-private example, because when the getter method tries to access this._secret, it accesses it on the proxy, which then gets it from the target. But that won't work with a truly private field, because the proxy object doesn't have the private field.

    The solution they provide is to change the get trap from the default above to:

    get(target, propName, receiver) {
        return target[propName];
        // The above is like `return Reflect.get(target, propName, target);`
        // Note `target` instead of `receiver` for third argument −^
    }
    

    That way, this in the getter method is target (aSecret), not receiver (proxy), and so accessing the private field works.

    Can a Proxy really provide access to private fields of a class?

    A Proxy can't expose the private fields of an instance that doesn't already provide a non-private way of accessing them, no. But it can provide access to getters and methods that have access to those fields. But you have to provide the right this.