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"
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 containingfirstName
,lastName
andage
-
prop
- the property that someone is trying toget
. In the example above,age
. -
reciever
- the proxy itself.
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
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 thenew
constructor keyword. -
proxy.defineProperty(object, prop, descriptor)
- a method to trap when a new property is added to an object usingObject.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 toget
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 thein
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 theObject.getPrototypeOf
method. -
proxy.getOwnPropertyDescriptor(object, prop)
- a method to trap thegetOwnPropertyDescriptor
, which returns a descriptor of a specific property - for example, is it enumerable, etc. -
proxy.isExtensible(object)
- a method to trap whenObject.isExtensible()
is fired. -
proxy.preventExtensions(object)
- a method to trap whenObject.preventExtensions()
is fired. -
proxy.setPrototypeOf(object, prototype)
- a method to trap whenObject.setPrototypeOf()
is fired. -
proxy.ownKeys(object)
- a method to trap when methods likeObject.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
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
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
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
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
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 trapsnew
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!
apply
accepts three arguments:
-
object
- the original object. -
thisObject
- thethis
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}
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.
Top comments (8)
Is there any real use for proxies? I can't imagine a situation in which this tool would be useful.
For the examples above, I usually create a class in which I pass the target and create an unlimited number of getters and setters for different needs, unlike one common for all as in Proxy.
Proxy is more about handling unknown field additions or array changes, like:
const obj = {}; obj.a = 'foo'
orconst list = []; list.push('bar')
. These operations are difficult to detect using class alone.Really just depends how you build things. There are many ways to achieve the same thing in Javascript.
The Immer library is made possible using Proxies.
Great example! Thank you.
The professional way to use proxies is to not use them at all. They're slow, and unless you're developing a library utilizing proxies (React, Svelte and Vue for example), I'd stay away.
There's some use cases, but there is also a lot of ways to achieve similar behavior without having to pay the price in performance. And I'm not talking nanoseconds. An array of 1000 proxies takes a painstakingly long time, we're talking seconds.
hmm really depends on your use case to be honest.. proxies performance is only going to be an issue if you have many thousands or millions of operations.
I have first hand experience of the performance cost, I'd stay shut otherwise.
I had been working on a library where it lets you translate an array of objects. The way it works is that the user supplies the translations and locales, if a locale was omitted the default was used. Now, writing the defaults directly on to the object felt messy and proxies seemed like a good solution. They weren't. For large collections (even as small as 1000 rows) the performance cost was noticeable, as said – seconds, and not acceptable.
A thing to keep in mind. Nevertheless, they do have their use cases and are a god sent for some libraries.