JS Proxy for Beginners

A powerful inbuilt utility to control your JavaScript objects

Hello fellow coders. Welcome back to my blog. Today we are talking about Proxy objects in JavaScript. Proxy objects are used quite widely and prove to be extremely useful in making things simple and easy to work with. Proxy object is one of the many inbuilt utilities in JavaScript and is supported out of the box in every browser and node version. We do not need to install an extra package or module to use them.

Definition

What does Proxy mean? Well, let us start with the English definition. The English word "proxy" means

  • the agency, function, or office of a deputy who acts as a substitute for another, or

  • a person authorized to act for another

Basically, when you ask someone else to express your interests or do something in your stead, they are representing you by proxy. This phenomenon is seen quite commonly in award shows when an actor accepts an award on behalf of their friend.

The idea is inherited in JavaScript as well. As per the MDN definition,

The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object.

So instead of directly working with your object where you can directly access, write or modify the data, you construct a Proxy object for that object and use that.

Let us take a look at a few examples to see the problems that arise from working with objects directly and how Proxy objects can solve them.

Problems with JS Objects

Typical JS objects have a couple of issues attached to them. Let us look at a couple of them before we learn how to solve them with Proxy.

For understanding purposes, we are going to be using the batman object as a common example for the coming code snippets.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};

Issue 1: Reading data

In order to read the data from the batman object, we can simply access the properties as

console.log(batman.name); // Bruce Wayne
console.log(batman['name']); // Bruce Wayne
console.log(batman.villains); // ["Joker", "Bane", "Riddler"]
console.log(batman['villains']); // ["Joker", "Bane", "Riddler"]

And as expected, the object would print the corresponding values to each of the properties.

However, there's a small issue. What if, the user tries to access batman.Name ? Now we know that the user wishes to access "Bruce Wayne" but technically, since the name of the property is different, it will point to a completely different entry. The capitalisation in the name of the property matters. Hence the user will get the output as undefined.

console.log(batman.Name);  // undefined
console.log(batman.Villains); // undefined

Issue 2 - Updating values

Updating the values in JavaScript objects is quite easy. We can simply refer to the property using its name and update the value.

batman.villains.append("Penguin");

console.log(batman.villains); // ["Joker", "Bane", "Riddler", "Penguin"]

However, there are huge problems with this. There are absolutely no validations present here. A user can simply redefine the value of any property when and wherever needed and unless some external validations are forced, the data inside any object can be modified in any way. Take the following snippet for example

batman.name = "Barry Allen";

Doesn't make sense in the context but JavaScript will allow it. Some properties are not actually meant to be updated and should not be touched but such validations are not here.

Issue 3 - Deleting keys

JavaScript allows you to delete any properties in your object simply by using the delete keyword

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};

delete batman.name;

console.log(batman.name); // undefined

However, this can potentially lead to several problems as some properties are not intended to be deleted. They are crucial to the basic logic and working of the object itself and must not be removed. Simply JS objects do not provide any validations to guard these properties or throw errors in case they are being deleted.

The Proxy Object

How to solve the above problems? The above problems can easily be solved using the JS Proxy. Instead of using the actual object, we will create a Proxy for it and use that Proxy. Here's a simple snippet to create a Proxy of the batman object. The below snippet doesn't solve any of the problems though, it simply creates a new Proxy.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {});

The Proxy constructor accepts two arguments here

  1. The target object - where we passed batman in the above example. This is the object we will be working on.

  2. The handler object - where we passed an empty object {} for now. This tells us how operations on target are supposed to be handled.

Let us build our handler step by step. Let us address the first issue i.e. Reading data

Reading values using Proxy

In our handler we can define a get method which will allow us to change the behaviour of how properties are read from our target object. Here's the default implementation of it.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {
    get(target, prop) {
        return target[prop];
    }
});

console.log(batmanProxy.name); // Bruce Wayne
console.log(batmanProxy.NAME); // undefined

The get method takes two arguments, the target object and the name of the property. However, the default implementation doesn't solve our problem to adjust for the uppercase. To do that, let us modify the snippet to read lowercase properties only.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {
    get(target, prop) {
        return target[prop.toLowerCase()];
    }
});

console.log(batmanProxy.name); // Bruce Wayne
console.log(batmanProxy.NAME); // Bruce Wayne

Now no matter in what case we pass the property, it will always provide us with a result as long as there is a property corresponding to it.

Updating values using Proxy

To control the update behaviour of our objects, we can define a set method in our handler as well.

Here's the default implementation of set method.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {
    get(target, prop) {
        return target[prop.toLowerCase()];
    },
    set(target, prop, value) {
        target[prop] = value;

        // Indicate success
        return true;
    }
});

console.log(batmanProxy.name); // Bruce Wayne
console.log(batmanProxy.NAME); // Bruce Wayne

batmanProxy.name = "Richard Grayson";

console.log(batmanProxy.name); // Richard Grayson

Now we can write some custom behaviour here as well to control which properties can be changed and which properties should not be changed. Let us say we wish the name property to be fixed.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {
    get(target, prop) {
        return target[prop.toLowerCase()];
    },
    set(target, prop, value) {
        // Avoid updating 'name' property
        if (prop === 'name') return false;

        // Default behaviour
        target[prop] = value;

        // Indicate success
        return true;
    }
});

console.log(batmanProxy.name); // Bruce Wayne
console.log(batmanProxy.NAME); // Bruce Wayne

batmanProxy.name = "Richard Grayson";

console.log(batmanProxy.name); // Bruce Wayne

Now we still get "Bruce Wayne" as the name despite trying to update it. The handler doesn't allow us to update the name property at all. If you wish, you can also throw errors here as you see fit.

Example:

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {
    get(target, prop) {
        return target[prop.toLowerCase()];
    },
    set(target, prop, value) {
        // Avoid updating 'name' property
        if (prop === 'name') {
            throw new Error('Cannot update name property');
        }

        // Default behaviour
        target[prop] = value;

        // Indicate success
        return true;
    }
});

console.log(batmanProxy.name); // Bruce Wayne
console.log(batmanProxy.NAME); // Bruce Wayne

batmanProxy.name = "Richard Grayson";

console.log(batmanProxy.name); // Error - Cannot update name property

Delete keys using Proxy

We can define a deleteProperty trap as well to control the deletion behaviour of our object.

Here is the default implementation of deleteProperty trap.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {
    get(target, prop) {
        return target[prop.toLowerCase()];
    },
    set(target, prop, value) {
        // Avoid updating 'name' property
        if (prop === 'name') {
            throw new Error('Cannot update name property');
        }

        // Default behaviour
        target[prop] = value;

        // Indicate success
        return true;
    },
    deleteProperty(target, prop) {
        delete target[prop];
        return true; // For success
    }
});

console.log(batmanProxy.name); // Bruce Wayne
console.log(batmanProxy.NAME); // Bruce Wayne

batmanProxy.name = "Richard Grayson";

console.log(batmanProxy.name); // Error - Cannot update name property

delete batmanProxy.villains;

console.log(batmanProxy.villains); // undefined

Let us say if we wish to prevent the villains property from being deleted. Then we can implement the following check.

const batman = {
    name: "Bruce Wayne",
    villains: ["Joker", "Bane", "Riddler"],
};
const batmanProxy = new Proxy(batman, {
    get(target, prop) {
        return target[prop.toLowerCase()];
    },
    set(target, prop, value) {
        // Avoid updating 'name' property
        if (prop === 'name') {
            throw new Error('Cannot update name property');
        }

        // Default behaviour
        target[prop] = value;

        // Indicate success
        return true;
    },
    deleteProperty(target, prop) {
        if (prop === 'villains') {
            // Can also throw error here, if needed
            return false;
        }
        delete target[prop];
        return true; // For success
    }
});

console.log(batmanProxy.name); // Bruce Wayne
console.log(batmanProxy.NAME); // Bruce Wayne

batmanProxy.name = "Richard Grayson";

console.log(batmanProxy.name); // Error - Cannot update name property

delete batmanProxy.villains; // Does not do anything now

console.log(batmanProxy.villains); // ["Joker", "Bane", "Riddler"]

Hence now we cannot delete the villains property. Deleting all other properties is still possible.

Conclusion

The Proxy object is a powerful JavaScript utility which allows us to set traps to control the default behaviour of Objects. It is especially useful for setting validations and important checks for our objects.

There are several other methods that can be implemented that were not covered in this article such as

  1. has

  2. defineProperty

  3. construct and many more.

Do go through the MDN official documentation for more information on Proxy object.

References

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects