Javascript Proxy Object, some examples
In javascript there is a very fancy object called the Proxy object which is used to intercept operations made on selected objects. This means that if you do something like
user.name = 'David'
you can have something else to happen instead of just the assignment of the string, or maybe you can make it so nothing happens at all.
So, let's try some examples. First thing we need an object to work with, we will use this very simple object:
let user = {
name: 'David',
age: 17
}
then we create a Proxy and wraps user
around it
let userProxy = new Proxy(user, {});
the first argument of the Proxy constructor is the target object, the second argument is the handler which is the object where we will list traps. Traps are functions that are invoked in place of the default operations on the target object.
We then can try a few things in the console:
user.name // returns 'David'
userProxy.name // also returns 'David'
userProxy.name = 'Jack'
user.name // now returns 'Jack'
as you can see the operations on the proxy called userProxy
happened on the target object user
. Nothing really new happened, so let's try to change the proxy object a little by adding a trap into the handler:
let userProxy = new Proxy(user, {
get(target, prop) {
return 'I am ' + target[prop]
}
});
with get()
we added a trap to intercept the internal method [[Get]]
and we changed its behavior. Now when we try
user.name // returns 'David'
userProxy.name // returns 'I am David!'
the behavior is different on the proxy than if we were to use the object.
Reassigning the same variable
You may not like the idea of having two different object so closely connected with each other and different behaviors. You could reassign user
so it contains the proxy directly
let user = {
name: 'David',
age: 17
}
user = new Proxy(user, {});
So now instead of having userProxy
and user
, we just have user
. This tecnique has nothing to do with proxies themselves, but it's useful to remember.
We want to create an object called post
that follows the following specification:
- when other programmers use this object, they can only set
title
andslug
(this-is-a-url-slug) as properties - only a properly formatted slug is accepted as a
slug
To make sure the behavior matches the specification we can set
trap on a proxy object. First let's see the most basic example of a set
trap:
let post = new Proxy({}, {
set(target, property, value) {
return target[property] = value
}
})
post.title = 'The origin of Ancient Rome'
Right now the behavior is very similar to that of a normal object. The string The origin of Ancient Rome
is the value
and is assigned to title
. Now let's try to develop a solution that matches our specification
let post = new Proxy({}, {
set(target, property, value) {
if(property == 'title') {
return target[property] = value
} else if(property == 'slug') {
return target[property] = value
} else {
throw new Error('Only "title" and "slug" are allowed as property');
}
}
})
post.title = 'The origin of Ancient Rome'
post.author = 'David Smith' // error
This code will throw an error when we try to set the author property. A programmer using this object will only be allowed to set title
and slug
. Now let's add a regular expression to make sure the slug
is properly formatted.
let post = new Proxy({}, {
set(target, property, value) {
if(property == 'title') {
return target[property] = value
} else if(property == 'slug') {
const regex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/g;
if(!regex.test(value)) {
throw new Error('The slug is not properly formatted');
}
return target[property] = value
} else {
throw new Error('Only "title" and "slug" are allowed as property');
}
}
})
post.title = 'The origin of Ancient Rome'
post.slug = 'Origin of Rome' // error
The object now follows the specification and will throw an error when the slug is not properly formatted. If we assigned origin-of-rome
as a slug, we wouldn't have had any error.
As you can see proxies can be really powerful (which also means dangerous). There are many traps that can be set matching the internal method of objects and functions. A list can be found on MDN and you can be refer to it when the need arises.
Reflect
The Reflect
object can be used anywhere but it goes hand in hand with proxies. Reflect
will match the default behavior of a target object.
let post = {
title: 'Geography of Japan'
}
Reflect.get(post, 'title'); // 'Geography of Japan'
post.title; // 'Geography of Japan'
Getting the property title using Reflect.get
or by writing post.title
has the same result, and this is useful in proxies because we overwrite the original behavior of the target object but we may still want to refer to it and Reflect
let us do so in a safe way.
let post = {}
post = new Proxy(post, {
set(target, property, value) {
console.log('Setting the property...')
return Reflect.set(target, property, value);
}
});
post.title = 'Geography of Japan';
instead of assigning the property on the target
by writing something like target[property] = value
we have used Reflect.set()
.
Internal methods that are trappable by Proxy
have the same method on Reflect
;
Receiver
In your code editor you may have noticed that a get
trap has a third argument called receiver
that we ignored up until now. The receiver
is the original object when the property lookup is performed.
let content = {
_title: '7 cookies recipes',
get title() {
return this._title;
}
}
console.log(content.title); // '7 cookies recipes'
content = new Proxy(content, {
get(target, property) {
return target[property];
}
})
console.log(content.title); // '7 cookies recipes'
let recipe = {
__proto__: content,
_title: 'Spaghetti',
}
console.log(recipe.title); // '7 cookies recipes'
In this example when we try to access recipe.title
we are moved to the get title()
on the content object, which right now is a proxy. The this
now refers to content
and we get the _title
in content
. Instead by using Reflect.get
and receiver
we can get the _title
in recipe.
let content = {
_title: '7 cookies recipes',
get title() {
return this._title;
}
}
console.log(content.title); // '7 cookies recipes'
content = new Proxy(content, {
get(target, property, receiver) {
return Reflect.get(target, property, receiver);
}
})
let recipe = {
__proto__ : content,
_title: 'Spaghetti'
};
console.log(recipe.title); // 'Spaghetti'
By using the receive
the original object we were trying to get the property from is remember.
Make some syntactic sugar
We have an array of animals and we want the programmers that use this array to be able to write something like 'dog' in animals
and get true or false. This syntactic sugar can be accomplished with proxies and the has
trap
let animals = ['lion', 'zebra', 'dog'];
animals = new Proxy(animals, {
has(target, valueToSearch) {
for(let element of target) {
if(element == valueToSearch) {
return true;
}
}
}
});
console.log('lion' in animals); // true
console.log('dog' in animals); // true
console.log('cat' in animals); //false
Attention! By default the in
operator behaves much differently, and what we are using as valueToSearch
would normally be the key
of the object. See this comparison:
let animals = ['lion', 'zebra', 'dog'];
console.log(0 in animals); // true, result of default behavior
animals = new Proxy(animals, {
has(target, valueToSearch) {
for(let element of target) {
if(element == valueToSearch) {
return true;
}
}
}
});
console.log(0 in animals); // false, result of modified behavior
Be careful when using this tecnique.
Make an Observable
function makeObservable(observed) {
let fns = [];
observed.observe = (fn) => {
fns.push(fn);
}
return new Proxy(observed, {
set(target, property, value, receiver) {
if(property == 'length') {
return Reflect.set(target, property, value, receiver);
}
let success = Reflect.set(target, property, value, receiver);
if(success) {
fns.forEach(fn => {
fn(property, value);
})
}
return success;
}
})
}
let animals = ['dog', 'cat', 'lion'];
animals = makeObservable(animals);
animals.observe((index, animal) => {console.log(`New animal spotted: ${animal}`)})