A Proxy object wraps another object and intercepts operations on it. While intercepting operations like reading, writing properties on the object, the proxy may choose to handle these operations and modify results.
Proxy
Syntax: let proxy = new Proxy(target, handler);
target
: the object that has to be proxied.handler
: the proxy configuration object, it may register traps
. A trap
is a handler for a particular kind of operation. By registering a trap
handler it can intercept the operation and do its own thing.
If there is a trap
for the operation onhandler
only then the operation will be trapped and handled by proxy else operation directly occurs on the object itself.
let user = {};
// target object -- object to be proxied
let userProxy = new Proxy(user, {});
// proxy for user, note empty handler
// operations on proxy
userProxy.name = 'Aniket';
// set operation
// should be intercepted by
// `set` trap on handler
// no `set` trap registerd so
// operations are performed on object itself
console.log(userProxy.name); // 'Aniket;
// get opertaion
// should be intercepted by `get` trap
// no `get` trap registerd so opertaion
// is performed on object directly
console.log(user.name); // 'Aniket'
// Thus we can see name property
// directly on target itself
For most of the operations on objects, there are “Internal Methods” in JavaScript that describes how operations work at a low level, what proxy trap does is that it can intercept these methods and do its own thing.
Below we show some of the “Internal Methods” and their corresponding proxy traps.
Internal Methods have some rules that our traps must follow, for eg: set
the trap must return true
if property setting was success else false.[[GetPrototypeOf]]
must always return the target’s prototype when applied on proxy as well.
The problem statement
It is common practice to use
*_*
is in the beginning of the property name to denote a private property. You cannot get/set/loop this property. Write a proxy to achieve this.
let user = {
name: 'Aniket',
_password: 'Password', // private property
isCorrectPassword(pswd) {
return this._password === pswd;
// `this` here is a gotcha
},
};
“set” trap
We will register a set
trap on the handler to intercept write operation on the object.
Syntax: set(target, prop, value, receiver).
target
: target object.prop
: property name that is being set.value
: the value of the property to be set.receiver
: the object that is utilised as in getters.
let userProxy = new Proxy(user, {
set(target, prop, value, reciver) {
// intercepts property write
if (prop.startsWith('_')) {
// check if property name start with `_`
// then it is a private property so
// don't allow to write or create a property
throw new Error("Access denied 💣 ");
} else {
target[prop] = val;
// normally write on object
return true; // must return true [[Set]] rule
}
}
});
“get” trap
We will register a get
trap to prevent direct access user._password
to private property. Also, we have to ensure that isCorrectpassword
works correctly as it does indirect access this._password
.
Syntax: get(target, property, receiver)
.
The arguments mean the same as above.
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
// intercept property read
if (prop.startsWith('_')) {
// if property name starts with `_` then
// we don't allow to read and raise an error
throw new Error("Access denied 💣 ");
} else {
// the property value may be a function or something else
let propValue = target[prop];
// in case it is a function
// it may have `this` inside it
// where `this` will ref to `userProxy`
// as it will be invoked as `userProxy.isCorrectPassword(pswd)`
// so `this == userProxy` but that will 🔥 our code
// so we need to make sure that our function `this` ref `user`
// and so we bind it
return (
typeof propValue === "function"
? propValue.bind(target) : propValue
);
}
}
});
“deleteProperty” trap
We will register deleteProperty
so that we can't delete a private property.
Syntax: deleteProperty(target, property)
let userProxy = new Proxy(user, {
deleteProperty(target, prop) {
// deleteProperty trap to handle property delete
if(prop.startsWith('_')) {
throw new Error("Access denied 💣 ");
} else {
// delete property on object
delete target[prop];
return true; // successfully deleted
}
}
});
“ownKeys” trap
for..in, Object.keys, Object.values
and other methods utilise an “Internal Method” called [[OwnPropertyKeys]]
to get a list of keys. For eg:Object.getOwnPropertyNames()
to get a list of non-symbol keys,Object.getOwnPropertySymbols()
to get a list of symbol keys,Object.keys()
to get a list of non-symbol enumerable keys, etc.
They all call [[OwnPropertyKeys]]
but tweak it a bit to return keys according to their use case. So we will register ownKeys(target)
trap to return only public keys.
let userProxy = new Proxy(user, {
ownKeys(target) {
// ownKeys will return a list of keys
// we must get keys on target then filter
// to remove all private keys
return Object.keys(target).filter((key)=>!key.startsWith('_'));
}
});
Note: Our traps must follow the rules defined for “Internal Method”. The rule defined for ownKeys
with Object.keys()
is that it must return non-symbol enumerable keys. Look at the example below to understand this gotcha.
let userProxy = new Proxy(user, {
ownKeys(target) {
// this will return list of keys
// and the calling method (Object.keys) tweak this list
// to select and return a list of
// non-symbolic and enumberable: true keys
// thus for each item in list returned by ownKeys
// it will only select item which is
// non-symbolic and enumberable: true
return ['email', 'phone'];
}
});
console.log(Object.keys(userProxy)); // [] empty 😱 gotcha
// solution
let userProxy = new Proxy(user, {
ownKeys(target) {
// Object.keys will check property descriptor
// for each key returned by ownKeys and see if
// enumberable: true
return ['email', 'phone'];
},
getOwnPropertyDescriptor(target, prop) {
// checking for enumberablity of keys
// is accessing its descriptor and seeing
// if enumberable is true
// here we are returning descriptor obj
// with enumberable true in all cases
return {
enumerable: true,
configurable: true,
};
}
});
“has” trap
This trap work with the in
operator that intercepts the [[hasProperty]]
Internal Method. Let’s register a has(target, property)
trap.
let range = {
from: 1,
to: 10,
};
// we need to check if 5 in range
// 5 in range if 5 >= range.from && 5 <= range.to
let rangeProxy = new Proxy(range, {
has(target, prop) {
// 5 >= 1 && 5 <= 10
return prop >= target.from && prop <= target.to;
},
});
console.log(5 in rangeProxy); // true
“apply” trap
Until now all examples we have seen were on objects and now we will see an example of function as target
.
Syntax: apply(target, thisArgs, args)
.thisArgs
: it is the value of this
args
: it is a list of arguments for function
// Let us write a function `delay`
// that delay exceution of any
// function `f` by `ms` milliseconds
// solution 1 closure way
function delay(f, ms) {
return function (name) { // *
setTimeout(() => f.bind(this, arguments), ms);
}
}
var hi = (name) => {
console.log('Hi! ' + name);
};
console.log(hi.length); // 1
// function.length returns number a params
hi = delay(hi, 3000);
// hi is now function at line *
console.log(hi.length); // 0 😱
// we lost orignal hi function
// and function at line * has no params so 0
hi('Aniket'); // 'Hi! Aniket'
// runs after 3s
// solution 2 proxy way
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArgs, args) {
setTimeout(() => target.bind(thisArgs, args), ms);
}
});
}
var hi = (name) => {
console.log('Hi! ' + name);
};
console.log(hi.length); // 1
hi = delay(hi, 3000);
console.log(hi.length); // 1 😎
hi('Aniket'); // 'Hi! Aniket'
The End
Now teach the Proxy you learnt here to your friend for whom you have put proxy 😂. Here is the next part of the post Part 2. Stay tuned for more content.