Photo by Dawid Zawiła on Unsplash
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
The
target
object - where we passedbatman
in the above example. This is the object we will be working on.The
handler
object - where we passed an empty object{}
for now. This tells us how operations ontarget
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
has
defineProperty
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