JavaScript Proxies
By: Heloise Parein
Tags: Coding, Programming, JavaScript, Software Development, Web Development

Proxies are an ES6 feature enabling to monitor how a given object is accessed. For example, let’s say we have an object alice that contains some information about Alice, like her birthdate, her age, her height, her weight and her BMI.

const alice = {

birthdate: '2000-04-06',

age: 20,

height: 170,

weight: 65,

bmi: 22.5,

};

Properties can be simply read and set by doing:

console.log(alice.height);

console.log(alice.age);

alice.weight = 64;

This object might be used by other parts of the code and a few problems can occur. We might want to let external consumers of the object change the weight of Alice, but not her birthdate. If she is a grown up, changing her height also doesn’t make much sense. We should recalculate the BMI if her weight changes. And her age should probably be calculated when being requested.

An idea would be to create methods like getAge or setWeight . That would partially work, but wouldn’t prevent anyone from simply doing alice.weight = 64; . JavaScript doesn’t natively have private fields.

A native JavaScript solution is to use a proxy. A proxy is simply a wrapper around your original object (called the target).

const handler = {};

const proxy = new Proxy(alice, handler);

You create it by calling the Proxy constructor, to which you pass the target, in our case alice , and a handler as an argument. The handler is an object in which we can define how each property should be accessed. In this example, our handler is empty, so the proxy just forwards every access request to the object.

console.log(proxy.age); // 20

console.log(proxy.height); // 170

Let’s now use the handler to define what is returned when the age property is read.

const handler = {

get (target, key) {

if (key === 'age') {

return calculateAge(new Date(target.birthdate));

}

return target[key];

}

};

const proxy = new Proxy(alice, handler);

Here is the calculateAge function if you are testing along:

const calculateAge = (birthdate) => {

const today = new Date();

let age = today.getFullYear() - birthdate.getFullYear();

const m = today.getMonth() - birthdate.getMonth();

if (m < 0 || (m === 0 && today.getDate() < birthdate.getDate())) {

age--;

}

return age;

}

Our handler now contains a get trap. When trying to read any property of the proxy, this get function is going to be called with the target ( alice ) and the key . When trying to read the age , instead of simply returning alice.age , we return a calculated value from the birhdate . For any other property, we just return target[key] .

You can test:

alice.age = 22;

console.log(proxy.age); // 20

console.log(proxy.height); // 170

We can also define a set trap, defining if and how properties should be set. For example we want to prevent the birthdate property to be set and we want to recalculate the BMI if the weight is changed:

const handler = {

get (target, key) {

...

},

set (target, key, value) {

if (key === 'birthdate') {

throw new Error('Birthdate is readonly');

} else if (key === 'weight') {

const { height } = target;

target.bmi = Math.round(

value / (height / 100 * height / 100) * 100

) / 100;

}

return true;

}

};

If you try to set a new birthdate you are now going to receive an error:

Every property is otherwise set to the value (this is what the return true; means). When the weight is being set we now recalculate the BMI. You can test that:

console.log(proxy.bmi); // 22.5

proxy.weight = 63;

console.log(proxy.bmi); // 21.8

We could do something similar for other readonly properties like age and height . We could also add _ in front of their names and write our set trap a way that an error is thrown for any property with a name starting with _ .

By the way, all this is only worth if you don’t give access to the alice object but only to the proxy . The alice object should therefore not be exported.

There are more traps than just set and get . You can for example define the behavior when:

a property is being deleted ( delete alice.height; ) with the deleteProperty trap.

) with the trap. it is being checked if a property exists on the object ( console.log('age' in proxy); ) with the has trap.

) with the trap. a new property is defined on the proxy ( proxy.name = 'Alice'; ) with the defineProperty trap.

There is a long list of possible traps, I just listed the most commons here. You can find the whole list here.