?
Digital Application Development
5 minute read

Exploiting JavaScript Proxies for Fun: Part 1 - Previewing Proxies

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.

All code in this article series is available on GitHub.

Proxies are one of the most powerful JavaScript features available but are rarely seen when actually writing code. This can be attributed to several reasons ranging from confusion to practicality, but two stand out to me as the most important:

  1. JavaScript is a powerful and versatile language even without Proxy.
  2. The use of a Proxy can easily obfuscate the true behavior of code, leading to confusion.

With these two points in mind, let's dive into what a Proxy actual is and what it can offer.

What is a proxy?

Proxies are a form of meta-programming and are fairly well named: they are a structure through which you can access or act on other structures. More simply, a proxy acts as a middleman representing an Object, Array, Function, etc. One of the simplest use-cases as an example of this would be using a Proxy to control what keys can be stored on an Object.

For instance, let's only allow keys in our object that follow the pattern yyyy-mm-dd:

// We could add a more robust date check if necessary, but this will do fine for now
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;

const target = {};

const handler = {
    set(target, prop, value) {
        if (dateRegex.test(prop)) {
            target[prop] = value;
        }

        return true; // indicate success regardless, false throws a TypeError
    }
};

const captainsLog = new Proxy(target, handler);

captainsLog['2020-01-01'] = 'I feel this is going to be a great year.';
captainsLog['2020-01-02'] = 'I no longer feel confident about this year.';
captainsLog['2020'] = 'I want to make a note about the whole year.';

console.log(JSON.stringify(captainsLog, null, 4));
/*
{
    "2020-01-01": "I feel this is going to be a great year.",
    "2020-01-02": "I no longer feel confident about this year."
}
*/

The third entry for 2020 is not present when we print the captainsLog. We've blocked the adding of the key 2020 by using a set function in our proxy handler. We only need to define the set function, as all other behavior about our captainsLog will remain the same as a normal object. There are many other functions we can define in our handler that will intercept other behaviors we might want to control including getting a prop (get), calling a function (apply) and deleting a prop (deleteProperty). For a full list and description of the possible functions, a proxy handler can have, check out MDN's Proxy writeup.

Why are proxies not common?

The above example looks like a great way to add mutation validation to objects, so why do we not see it used more often? Surely validating the data going into objects is a common enough task?

One reason is that JavaScript also introduced the ability to define setters directly on objects, like so:

class MyClass {
    constructor(value) {
        this._value = value;
    }
    
    get value() {
        return this._value;
    }
    
    set value(newValue) {
        // validation here
        this._value = newValue;
    }
}

True, we cannot validate a key for the class, but we can validate the value being passed in, which is a far more common scenario.

The other reason is that proxies obfuscate what is really happening in the code. For a developer looking at the code above, there is no reason to assume that the captainsLog is anything but an ordinary object if the declaration were moved off into some other file. If we want a developer to immediately know that there is more to these few lines of code than meets the eye, we should try to make it clearer. Which line of code makes you think there is code elsewhere being run?

captiansLog['2020'] = 'I want to make a note about the whole year.';
// vs.
captainsLog.set('2020', 'I want to make a note about the whole year.');

The function call indicates that there may be more going on here and we should examine the internals of the set call. The proxy, however, cannot be inferred simply by reading the direct assignment. We could try to remedy this a bit by throwing an error on an invalid key instead of ignoring the assignment, but that still requires the code to be run in order to encounter it. With the function call, we can immediately go to find the implementation and realize that 2020 will fail the validation check and make the appropriate change.

When should I use proxies?

What purposes are proxies best suited for then? In most cases, not much. Meta-programming is usually much more helpful in languages that aren't dynamically typed or are not nearly as flexible as JavaScript already is. I have seen maybe one or two scenarios in which a Proxy would be considered a reasonable approach in an application, but even in those cases, there was also the option of refactoring to avoid use. There are more potential uses in the area of creating utilities or libraries as a pattern of use can be more easily controlled and documented. But ultimately, I think a great quote to keep in mind when considering using a Proxy is…

That being said, proxies can be a lot of fun and I strongly encourage you to play around with them. Proxies are all about enhancing the programming experience and there are some truly creative uses for proxies. Finding ways to make your own life easier or your fellow teammates' lives easier is always a win.

What's next?

In future parts of this series, We will explore some of the interesting ideas made possible through proxies. In Part 2, we will create a proxy that allows us to simulate private properties in JavaScript classes. As a reminder, all code in this article series is available on GitHub.