?
Digital Application Development
5 minute read

Exploiting JavaScript Proxies for Fun: Part 2 - Private (Proxy) Party

Proxies were added to JavaScript in 2015, but still are a rare sight when developing. This article series will take a look at why that might be and some of the fun ways proxies can be used to enhance your code.

If you haven't yet, check out Part 1 of this series for an overview of JavaScript proxies. All code in this article series is available on GitHub.

JavaScript doesn't currently have the concept of private fields in classes (this.#yet), but that doesn't mean there aren't ways to hide values. A common approach is to create closures to capture values that should not be exposed but can still be accessed by methods. Another common approach is to simply use the prefix of _ on any fields in a class that should be regarded as private, though this does not stop outside actors from accessing them. When using modules, you can also offload your private values to an Object, Map, or WeakMap that uses something unique as a key to retrieve those properties.

But what if we could use a proxy to make classes behave as though they truly have private properties? We would need to hide those properties from being accessed outside, but also still allow the class and its methods to view and modify them. For a simple starting point, we will assume privacy by the convention mentioned above: the property starts with a _. Here is a sample class that picks a random number in between 0 and a maximum and allows you to guess what it is:

class Mystery {
    constructor(max) {
        this._max = max;
        this._value = Math.ceil(Math.random() * max);
        this._guessed = false;
    }

    get guessed() {
        return this._guessed;
    }

    reset() {
        this._guessed = false;
        this._previous = this._value;
        this._value = Math.ceil(Math.random() * this._max);
    }

    guess(value) {
        if (this._value === value) {
            return this._guessed = true;
        }

        return false;
    }
}

const instance = new Mystery(10);

We have several properties that we indicate should be private and use two methods and one getter to access them internally. Now that we have an instance of our class, it is very simple to set up a proxy around it to pretend as though those certain properties don't exist:

const privateInstanceProxy = {
    get(target, prop) {
        if (prop.startsWith('_')) {
            return undefined;
        }

        return target[prop];
    },
    set(target, prop, value) {
        if (!prop.startsWith('_')) {
            target[prop] = value;
        }

        return true;
    },
    has(target, prop) {
        return !prop.startsWith('_') && (prop in target);
    },
};

const proxied = new Proxy(instance, privateInstanceProxy);

As you can see, all we've done is define that whenever a property starts with _ we act as though you cannot get, set, or check that the instance has that property.

This helps a little, but it would be annoying to have to wrap a new Proxy around every instance we create. We can simplify this by instead targeting the class itself with a proxy that intercepts the construct functionality:

const privateClassProxy = {
    construct(target, args) {
        const instance = new target(...args);

        return new Proxy(instance, privateInstanceProxy);
    }
};

Mystery = new Proxy(Mystery, privateClassProxy);

Now any time we call to create a new Mystery, it will return a new instance wrapped in our privacy proxy! There's still another issue to address though: none of the methods in our class can access the private properties. This is because we are technically calling them with the proxied version of the instance as the this argument rather than the underlying instance itself. We can remedy this by binding each of the methods of each newly created instance to itself before the proxy is applied:

const privateClassProxy = {
    construct(target, args) {
        const instance = new target(...args);

        const methods = Object.getOwnPropertyNames(target.prototype).filter(prop => {
            return prop !== 'constructor' && instance[prop] instanceof Function
        });

        methods.forEach(method => instance[method] = instance[method].bind(instance));

        return new Proxy(instance, privateInstanceProxy);
    }
};

Now when we test it out:

const instance = new Mystery(10);

console.log('instance._value:', instance._value); // undefined
console.log('instance._guessed', instance._guessed); // undefined
console.log('instance.guessed', instance.guessed); // false

for (let guess = 1; guess <= 10; guess++) {
    console.log('guessing:', guess, instance.guess(guess)); // 1 true, rest false
}

console.log('instance.guessed before reset:', instance.guessed); // true

instance.reset();

console.log('instance.guessed after reset:', instance.guessed); // false
console.log('is _previous in instance:', '_previous' in instance); // false

Voila! We can now have private properties in classes!

It is worth noting that there are some oddities with this setup that have not been addressed. One is that there are a few more proxy handler functions we should probably define to fully hide the properties on the class from being discovered, but given this is JavaScript and anyone would be able to look at the source code if they are running it, the main blocking of get and set are what is needed. 

Another is that parent classes will have access to private properties as will any function (internal to the class or otherwise) that you pass this to directly. This is more of just a side effect of how this privacy is created than an actual problem and could be remedied with some more work. 

Finally, there may be some concern over the proxy assuming we should bind to this for every method since it limits the use of these methods. I would argue this is a common enough practice as it is and needing the method to be bindable to another this argument probably indicates that it should not be tied to the class in the first place.

In Part 3 of the series, we will take a look at using a proxy to build reactive code. As a reminder, all code in this article series is available on GitHub.