Javascript Proxy: Using Javascript Proxies like a Pro

Johnny Simpson - Oct 22 '22 - - Dev Community

Proxies are objects in Javascript which allows you to make a proxy of an object, while also defining custom behaviour for standard object operations like get, set and has. What that means is that, for example, you can define a set of custom behaviour should someone try to get the value of a property from an object. This turns proxies into quite a powerful tool, so lets look at how they works.

The basics of Javascript Proxies

The above sounds quite complicated, so lets look at a simple example without any methods, to begin with. Proxies can be created using the new Proxy() constructor, which accepts two arguments:

  • the target, which is the original object.
  • the handler, which is the set of methods or properties we will add on top of our object.

The handler can contain a list of predefined methods. If we define a method for get, for example, it will customise what happens when we try to get an item from an object.

let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    get: (object, prop) => {
        console.log(`Hi ${object.firstName} ${object.lastName}`)
    }
}

let proxyExample = new Proxy(target, handler);

proxyExample.age; // console logs "Hi John Doe"
Enter fullscreen mode Exit fullscreen mode

Since we tried to get the value of proxyExample.age on our proxy, the custom get handler fired - so we console logged Hi ${object.firstName} ${object.lastName}. As you can see, this can become quite a powerful tool, as you can do all sorts of stuff when standard operations of an object are called.

Notice that when we added get to the handler above, we had some custom arguments. Each handler you can add to a proxy comes with a set of custom arguments.

For get the function used is get(object, prop, receiver):

  • object - the original object. In the example above, this is the object containing firstName, lastName and age
  • prop - the property that someone is trying to get. In the example above, age.
  • reciever - the proxy itself.

!Handler methods are called "traps"

Updating Proxy Values

Proxies still refer to the original object, so the reference is the same for both the object values and the proxy values. As such, if you try to update the value of a proxy, it will also update the value of the original object. For example, below I try to update the proxy and as you can see, both the original object and the proxy are updated:

let target = {
    name: "John",
    age: 152
}

let handler = {
}

let proxyExample = new Proxy(target, handler);
proxyExample.name = "Dave";

console.log(proxyExample.name); // Console logs Dave
console.log(target.name); // Console logs Dave
Enter fullscreen mode Exit fullscreen mode

This is useful to know - don't expect that a proxy will create a separate object completely - it is not a way to make copies of objects.

Custom Handlers in Javascript Proxies

Proxies have a number of custom handlers allowing us to basically "trap" any object operation and do something interesting with it. The most commonly used methods are:

  • proxy.apply(objects, thisObject, argList) - a method to trap the function call.
  • proxy.construct(object, argList, newTarget) - a method to trap when a function is called with the new constructor keyword.
  • proxy.defineProperty(object, prop, descriptor) - a method to trap when a new property is added to an object using Object.defineProperty.
  • proxy.deleteProperty(object, prop) - a method to trap when a property is deleted from an object.
  • proxy.get(object, prop, receiver) - as described before, a method to trap when someone tries to get a property from an object.
  • proxy.set(object, prop, value, receiver) - a method to trap when a property is given a value.
  • proxy.has(object, prop) - a method to trap the in operator.

The methods above are enough to do pretty much everything you ever want to do with proxies. They give you pretty good coverage of all major object operations, to modify and customise as you like.

There are a few more though - so as well as these pretty fundamental object operations, we also have access to:

  • proxy.getPrototypeOf(object) - a method to trap the Object.getPrototypeOf method.
  • proxy.getOwnPropertyDescriptor(object, prop) - a method to trap the getOwnPropertyDescriptor, which returns a descriptor of a specific property - for example, is it enumerable, etc.
  • proxy.isExtensible(object) - a method to trap when Object.isExtensible() is fired.
  • proxy.preventExtensions(object) - a method to trap when Object.preventExtensions() is fired.
  • proxy.setPrototypeOf(object, prototype) - a method to trap when Object.setPrototypeOf() is fired.
  • proxy.ownKeys(object) - a method to trap when methods like Object.getOwnPropertyNames()is fired.

Let's look at some of these in a bit more detail to understand how proxies work.

 Using the in operator with Proxies

We have already covered proxy.get(), so lets look at has(). This fires primarily when we use the in operator. For example, if we wanted to console log the fact that a property does not exist when in is used, we could do something like this:

let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    has: (object, prop) => {
        if(object[prop] === undefined) {
            console.log('Property not found');
        }
        return object[prop]
    }
}

let proxyExample = new Proxy(target, handler);

console.log('address' in proxyExample); 
// console logs 
// 'Property not found' 
// false
Enter fullscreen mode Exit fullscreen mode

Since address is not defined in target (and thus in proxyExample), trying to console log 'address' in proxyExample will return false - but it will also console log 'Property not found', as we defined that in our proxy.

Setting values with proxies

A similarly useful method you may want to modify is set(). Below, I use the custom set handler to modify what happens when we try to change a user's age. For every set operation, if the property is a number, then we'll console log the difference when the number is updated.

let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    set: (object, prop, value) => {
        if(typeof object[prop] === "number" && typeof value === "number") {
            console.log(`Change in number was ${value - object[prop]}`);
        }
        return object[prop]
    }
}

let proxyExample = new Proxy(target, handler);

proxyExample['age'] = 204;
// Console logs 
// Change in number was 52
Enter fullscreen mode Exit fullscreen mode

Since both proxyExample.age and the updated value 204 are numbers, not only do we update our value to 204, but we also get a useful console log telling us what the difference between the two numbers is. Pretty cool, right?

While set will fire for any set operation, including adding new items to an object, you can also achieve similar behaviour with defineProperty. For example, this will also work:

let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    defineProperty: (object, prop, descriptor) => {
        console.log(`A property was set - ${prop}`);
    },
}

let proxyExample = new Proxy(target, handler);

proxyExample['age'] = "123 Fake Street";
// Console logs
// A property was set - address
Enter fullscreen mode Exit fullscreen mode

However please note that should you add set and defineProperty both as handlers, set will override defineProperty in situations where we set properties using square bracket [] or . notation. defineProperty will still fire if you use Object.defineProperty explicitly, though, as shown below:

let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    defineProperty: (object, prop, descriptor) => {
        console.log(`A property was set with defineProperty - ${prop}`);
        return true;
    },
    set: (object, prop, descriptor) => {
        console.log(`A property was set - ${prop}`);
        return true;
    },
}

let proxyExample = new Proxy(target, handler);

Object.defineProperty(proxyExample, 'socialMedia', {
    value: 'twitter',
    writable: false
});
proxyExample['age'] = "123 Fake Street";
// Console logs
// A property was set with defineProperty - socialMedia
// A property was set - address
Enter fullscreen mode Exit fullscreen mode

Deleting values with proxies

As well as these useful methods, we can also use deleteProperty to handle what happens if the user uses the delete keyword to remove something. For example, we could console log to let someone know that properties are being deleted:

let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    deleteProperty: (object, prop) => {
        console.log(`Poof! The ${prop} property was deleted`);
    },
}

let proxyExample = new Proxy(target, handler);

delete proxyExample['age'];
// Console logs
// Poof! The age property was deleted
Enter fullscreen mode Exit fullscreen mode

Customising function calls with proxies

Proxies also allow us to run custom code when we want to call a function. This is because of the Javascript quirk of functions being objects. There are two ways to do this:

  • with the apply() handler, which traps standard function calls.
  • with the construct() handler, which traps new constructor calls.

Here's a quick example where we trap a function call, and modify it by appending something to the end of its output.

let target = (firstName, lastName) => {
    return `Hello ${firstName} ${lastName}`
}

let handler = {
    apply: (object, thisObject, argsList) => {
        let functionCall = object(...argsList);
        return `${functionCall}. I hope you are having a nice day!`
    },
}

let proxyExample = new Proxy(target, handler);

proxyExample("John", "Doe");
// Returns
// Hello John Doe. I hope you are having a nice day!
Enter fullscreen mode Exit fullscreen mode

apply accepts three arguments:

  • object - the original object.
  • thisObject - the this value for the function/object.
  • argsList - the arguments passed to the function.

Above, we called our function using the object argument, which contains the original target function. Then we added some text onto the end of it to change the output of the function. Again, pretty cool, right?

We can also do the same using construct, which also has three arguments:

  • object - the original object.
  • argsList - the arguments for the function/object.
  • newTarget - the constructor that was originally called - i.e. the proxy.

Here's an example where a function returns an object, and we add a few more properties onto it using the construct method on our proxy:

function target(a, b, c) {
    return { 
        a: a,
        b: b,
        c: c
    }
}

let handler = {
    construct: (object, argsList, newTarget) => {
        let functionCall = object(...argsList);
        return { ...functionCall, d: 105, e: 45 }
    },
}

let proxyExample = new Proxy(target, handler);

new proxyExample(15, 24, 45);
// Returns
// {a: 15, b: 24, c: 45, d: 105, e: 45}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Proxies are an amazing tool in your Javascript arsenal which let you modify the basic operations of objects. There are a tonne of methods here to play around with and they can greatly simplify your code if you use them correctly. I hope you've enjoyed this article - you can read more of my Javascript content here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .