While learning about various JavaScript features, it is usually not crucial to know them by heart. Sometimes it is very beneficial just to know what’s out there and what can we use it for. One of those features is a JavaScript Proxy. In this article, we go through its functionalities and figure out some of its use-cases.
The purpose of JavaScript Proxy
The core concept of a Proxy is that it aims to define custom behavior for basic operations. It is not only one of the ES6 features that we can use to change some of the ways the JavaScript language operates. In one of the previous articles, we also talk about how we change some of the language behavior with symbols.
1 |
const proxy = new Proxy(target, handler); |
The code above contains a few concepts that we need to get the hang of to work with Proxies.
The first of them is a target. It is an already existing object – we want its behavior to change in some way. It can be any object you want – an array, a function, or another JavaScript proxy.
The other is a handler. It contains methods that alter the behavior of the target. We refer to the above functions as traps.
The name trap comes from the concept within the operating system.
get
One of the most basic examples is a get trap. It intercepts the operation of getting a property value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const translations = { helloWorld: 'hello world!', elementsListDescription: 'this is the description of a list of elements' }; console.log(translations.helloWorld); // hello world! console.log(translations.elementsListDescription); // this is the description of a list of elements console.log(translations.elementsListHeader) // undefined const proxy = new Proxy(translations, { get(target, property) { return property in target ? target[property] : 'default translation' } }); console.log(proxy.helloWorld); // hello world! console.log(proxy.elementsListDescription); // this is the description of a list of elements console.log(proxy.elementsListHeader) // default translation |
In the above code, we create a proxy that extends the behavior of the translations object. Now, if the desired string is not found, we get a default translation.
set and defineProperty
Other traps that might come in handy are set and deleteProperty. With them, we can change the behavior of delete operator and how setting the properties works.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const myObject = {}; const proxy = new Proxy(myObject, { set(target, property, value) { if (property === 'resizeListener') { target[property] = value; window.addEventListener('resize', value); return true; } }, deleteProperty(target, property) { if (property === 'resizeListener' && property in target) { window.removeEventListener('resize', target[property]); return delete target[property]; } return false; } }) proxy.resizeListener = () => { console.log(`${window.innerHeight} x ${window.innerHeight}`) } |
The
set
method should return a boolean value. Returntrue
to indicate that assignment succeeded.The
deleteProperty
method must return aBoolean
indicating whether or not the property has been successfully deleted.
In the simple example above, we have an object that stores a resize listener. It might make sense to automatically attach the event listener to the window object when we set the resizeListener property and clean up when we delete it.
has
In the examples above, we use the in operator. We can change its behavior with a proxy!
A common approach to private fields in JavaScript is to add an underscore in the property name. We might want to hide them for the in operator.
The above way of defining private fields in JavaScript might soon change due to introducing native private fields
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const myObject = { _privateField: 'a secret!', dogName: 'fluffy' }; const proxy = new Proxy(myObject, { has (target, key) { return key[0] === '_' ? false : key in target; } }) console.log('_privateField' in proxy); // false console.log('dogName' in proxy); // true |
There are more ways in which you can use a proxy. For a complete list visit the MDN documentation.
The real-world use case
You might say that using a JavaScript proxy might make the code less readable. After all, somebody might not expect that there is some additional logic behind a simple get operation (even though we also have a concept of getters). I would agree in a lot of cases. Keeping the code simple is a lot more critical than using fancy language features.
Instead of creating a proxy to provide default translation, we could create a straightforward function. Some might argue that this is more readable than creating a proxy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const translations = { helloWorld: 'hello world!', elementsListDescription: 'this is the description of a list of elements' }; function getTranslation(string) { return string in translations ? translations[string] : 'default translation' } console.log(getTranslation('helloWorld')); // hello world! console.log(getTranslation('elementsListDescription')); // this is the description of a list of elements console.log(getTranslation('elementsListHeader')); // default translation |
On the other hand, I recently found a bug in a library called lcid. Fixing it required changing the data structure that was the basis of the whole library. The main issue was that the data structure is also exported from the library. Since lcid is currently downloaded about 13 million times per week, I wanted to maintain some level of backward compatibility when creating a pull request.
Previously, the data structure looked like that:
1 2 3 4 5 |
{ "en_US": 1033, "es_ES": 3082 // (...) } |
I basically reverted it:
1 2 3 4 5 6 |
{ "1033": "en_US", "1034": "es_ES", "3082": "es_ES" // (...) } |
Since the whole data structure was exported so that anyone can use it as they see fit, two problems emerged:
- the data structure is now inverted
- the keys are now strings, not numbers
To fix the above, I inverted the data structure again programmatically and wrapped it in a proxy:
1 2 3 4 5 6 7 8 9 10 11 |
exports.all = new Proxy( inverted, { get(target, name) { const lcid = target[name]; if (lcid) { return Number(lcid); } } } ); |
Our proxy performs a conversion from a string to the number when a get operation takes place. Thanks to that, even though we modify the underlying data structure, we keep some level of backward compatibility.
1 |
console.log(all['en_US']); 1033 |
Summary
The most important thing to remember, in my opinion, is to try not to reinvent the wheel. If we can achieve something can with a more conventional approach, there is a good chance that the code will be more readable. If you keep that in mind, you still have a good chance of encountering a good use case for using a JavaScript proxy. My aim with that article was for you to be aware of what can you achieve with it and increase your JavaScript toolset.