Javascript Proxy: Using Javascript Proxies like a Pro
📣 Sponsor
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.
Handler methods are called “traps”
Handler methods in a proxy are typically called traps, so don’t be confused if you see this term thrown around. They are called this way because they “trap” object operations and perform some kind of custom code. For example - console logging something everytime someone tries to get a property of a proxy.
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.
More Tips and Tricks for Javascript
- Removing the last element of an array in Javascript
- Javascript Operators and Expressions
- Demystifying how 'this' works in Javascript
- How to use Environmental Variables in NodeJS
- How to validate an email with Javascript
- A Guide to Heaps, Stacks, References and Values in Javascript
- How to fix 'Uncaught SyntaxError: Cannot use import statement outside a module'
- Scheduling and Runnning Recurring Cron Jobs in Node.JS
- Web Workers Tutorial: Learn how Javascript Web Workers Work
- Sharing Screens with the New Javascript Screen Capture API