
One of the unique features in ES6 is the addition of proxy objects. The proxy object is an object that wraps another object to defined particular operations on that object.
If you heard about the term interception before in other languages or tools. A proxy object acts as an interceptor that wraps another object which intercepts a set of operations like reading that object or writing to that object. The term “proxy” because it forwards the calls to the target object. Many frontend frameworks and technologies uses the Proxy object like Vuejs.
These operations are somewhat called “traps” and it much like magic methods in languages such as PHP which trigged automatically when specific event happens, for example when setting a property to a proxy object the set() trap is triggered.
Terminology of Proxy Object
const proxy = new Proxy(target, handler);
target
– The original object to wrap, can be an object, a function or even class.- handler– The object that holds the operations (traps) that control the behavior of the proxy in react to specific event.
Basic example:
const user = { firstName: "John", lastName: "Doe", age: 35 } let proxy = new Proxy(user, {}); console.log(proxy);
As you i created a proxy using the new Proxy() constructor and passing the target object and the handler. The handler can be empty as in this example and any operation performed on the Proxy forwarded to the original object.
For example we can read and write to the proxy as if it is the original object:
console.log(`My name: ${proxy.firstName} ${proxy.lastName}`); // My name: John Doe
proxy.firstName = 'Wael'; console.log(proxy.firstName); // Wael console.log(user.firstName); // Wael
As you see here reading and writing to the proxy is the same as reading and writing to the target object. So what’s the point of using Proxy? The point is by customizing the proxy behavior using operations or traps, let’s see this below.
Proxy Traps
Proxy objects takes it’s power from the operations defined in the handler object, these operations called “traps”. From these operations:
get()
: The get() trap triggered automatically when a property of the target object accessed via a proxy object.set():
The set() trap triggered when attempting to set a property of the target via proxy.apply():
The apply() trap is triggered when attempting to make a function call, it’s like javascript’s apply() function.
get()
Trap
Using the previous example let’s define a handler with the get() trap:
const handler = { get(target, property, receiver) { console.info(`${property} accessed`); return target[property]; } }; let proxy = new Proxy(user, handler); console.log(`My name: ${proxy.firstName} ${proxy.lastName}`); // output firstName accessed lastName accessed My name: John Doe
As you see in the output two console statements has been output when reading both proxy.firstName and proxy.lastName. The get() trap accepts three parameters (target, property, receiver). The “target” represents the original object and property represents the accessed property via the proxy.Â
The typical implementation to retrieve property value is by using target[property]. In fact this is the default implementation when no get() trap defined in the handler.
Let’ customize the get() trap by returning an arbitrary string:
get(target, property, receiver) { console.info(`${property} accessed`); return "Annonymous"; } // output My name: Annonymous Annonymous
Or you can check for a property:
get(target, property, receiver) { console.info(`${property} accessed`); if(property === 'firstName') { return "Wael"; } else if(property === 'lastName') { return "Salah"; } return "Annonymous"; }
The benefit of the get() trap on proxy objects is to to generate computed properties. For example if we want to get the formatted user data as a string in the previous example:
get(target, property, receiver) { if(property === 'userDetails') { return `First name: ${target['firstName']}\nLast name: ${target['lastName']}\nAge: ${target['age']}`; } else { return target[property]; } } console.info(proxy.userDetails); console.info(proxy.firstName); // output First name: John Last name: Doe Age: 35 John
Example: Prevent access to particular property:
const user = { .... ...., balance: '100$' // protected property } get(target, property, receiver) { if(property === 'balance') { throw new Error(`Attempt to access protected property ${property}`); } else { return target[property]; } } console.info(proxy.balance); // output Uncaught Error: Attempt to access protected property balance
set()
Trap
The set() trap fired when attempting to set a property on proxy.
Let’s add a set() trap to the previous example:
let handler = { set(target, property, value) { target[property] = value; return true; } };
This is the simplest form of the set() trap which is to the set the value over the property on the target. The set() trap accepts there parameters (target, property, value) which much the same as the get() trap. Most important thing to note that the set() must return true on successful write and false or exception on unsuccessful write.
Of course this is trivial use of set() trap. The most common use of the set() trap is by doing validations on the target object.
Suppose that we want to validate the user data using specific constraints:
let handler = { set(target, property, value) { if(property === 'age') { if(isNaN(value)) { throw new Error(`age must be number`); } if(value > 40) { throw new Error(`age must not be bigger than 40`); } if(value < 20) { throw new Error(`age must not be less than 20`); } } if(property === 'firstName' || property === 'lastName') { if(value.length > 30) { throw new Error(`${property} length must not exceed 30 characters`); } } target[property] = value; return true; } }
proxy.age = 'not number'; proxy.firstName = 'too long stringggggggggggggggggggggggggggggggggggggggggggggggggggggggg'; console.log(proxy.age); console.log(proxy.firstName); // output Uncaught Error: age must be number Uncaught Error: firstName length must not exceed 30 characters
Also using the set() trap we can disallow writing to specific property on the target object the same as get().
So in the above example suppose we want to prevent setting the user balance, we can easily add appropriate logic like so:
set(target, property, value) { if(property === 'balance') { throw new Error(`Attempt to set protected property ${property}`); } target[property] = value; return true; }
apply()
Trap
The apply() trap is function call trap, which means when the target in this case is a function object. It fired when attempting to call the proxy as a function or calling the native js apply() function on the proxy.
const handler = { apply: function(target, thisArg, argumentsList) { } };
The apply() trap accepts three arguments (target, thisArg, argumentsList).
Example: sum numbers using apply():
let handler = { apply(target, thisArg, argumentsList) { console.log(argumentsList); return argumentsList.reduce((prev, a) => prev + a, 0); } } const sumProxy = new Proxy(function() {}, handler); console.log(sumProxy(10, 20, 30));
In this example we have access to arguments list passed when calling the proxy as a function, As you see i applied the javascript reduce() function on the array of arguments to do the sum operation.
Example2: using the previous user example, By having knowledge that the user object have a balance property and we want to increment or decrement this balance using apply():
const user = { firstName: "John", lastName: "Doe", balance: 1000 } function incrementBalance(user) { user.balance++; return user.balance; } function decrementBalance(user) { user.balance--; return user.balance; } let handler = { apply(target, thisArg, args) { return target(...args); } } let incrementProxy = new Proxy(incrementBalance, handler); let decrementProxy = new Proxy(decrementBalance, handler); console.info(incrementProxy(user)); console.info(incrementProxy(user)); console.info(decrementProxy(user)); console.info(decrementProxy(user)); console.info(user); // output 1001 1002 1001 1000 Object { firstName: "John", lastName: "Doe", age: 35, balance: 1000 }
deleteProperty()
Trap
The deleteProperty() trap is a trap fired when calling the delete operator on proxy object to remove specific property. Again we can use this to allow or disallow deleting properties on the target object.
const proxy = new Proxy({}, { deleteProperty: function(target, prop) { if (prop in target){ delete target[prop]; console.info(`property deleted: ${prop}`); return true; } else { console.error(`property not found: ${prop}`) return false; } } }); delete proxy.foo; proxy.foo = 'bar'; delete proxy.foo; // output property not found: foo property deleted: foo
The deleteProperty() trap accepts two arguments (target, prop). You should add the appropriate to delete a property like in the example above. The trap must return a boolean value indicating successful delete or error.
has()
Trap
The has() trap fired when using the in operator to check for property existence on an object.
A typical example when you have a protected property starting with _ to hide it from specific users.
const handler = { has(target, prop) { if (prop[0] === '_') { return false; } return prop in target; } }; const user = { _password: '*****', views: 100 }; const proxy = new Proxy(user, handler); console.log('views' in proxy); console.log('_password' in proxy); // output true false
The has() trap accepts two arguments (target, prop). As you see when using the in operator to check for property on the proxy the has() trap is intercepted and appropriate logic applied. This trap must return a boolean indicating for property existence or not.
Other Traps You Can Check
ownKeys
getOwnPropertyDescriptor
defineProperty
preventExtensions
isExtensible
getPrototypeOf
setPrototypeOf
Proxy Objects and Plain Objects
Proxy objects can be converted back to the plain version of this object using several techniques, for example the destruction and spread operator can be used:
const plainObject = { ...proxyObject };
Also other solutions is to use JSON.parse() function:
JSON.parse(JSON.stringify(proxyObject));