ECMAScript 6 introduced a number of new language features to JavaScript, amongst them were proxies. Which are, in my opinion, the most underrated feature of JavaScript.
Proxies enable us to do runtime meta-programming by allowing us to intercept and redefine the behaviour for intrinsic operations such as property getters, setters, value assignments, call operations and so on.
Now the actual, real-world, practical good use cases for proxies are few and far between. In most cases, the same thing can be achieved with a bit of repetitive boilerplate code with far better performance. Still, proxies are great and incredibly powerful. Let’s have a look at some terrible use cases to show just how magical proxies can be.
Forgiving property names
One of the operations we can override is an object’s property getter. So let's use that to provide an auto-correcting property lookup using the Levenshtein distance to approximate what the user’s intended property name was.
First things first, we need to define a function to return the Levenshtein distance between two strings. The Levenshtein distance is essentially a measurement of the minimum number of single-character edits (insertions, deletions or substitutions) required to change one string into the other.
We’ll do the recursive variant because it’s straightforward and easier to follow than a more optimized one. However, it should be noted that it’s also extremely inefficient compared to an iterative approach with lookup tables:
function levenshtein(a, b) {
if (a.length == 0) {
return b.length;
}
if (b.length == 0) {
return a.length;
}
let cost = (a.charAt(a.length - 1) == b.charAt(b.length - 1)) ? 0 : 1;
return Math.min(
levenshtein(a.substring(0, a.length - 1), b) + 1,
levenshtein(a, b.substring(0, b.length - 1)) + 1,
levenshtein(a.substring(0, a.length - 1), b.substring(0, b.length - 1)) + cost,
);
}
With the Levenshtein distance figured out, it’s fairly trivial to get the closest matching property name by reducing an array of property names to the string with the shortest distance to the target property:
function getClosestPropertyName(names, name) {
let lowest = Infinity;
return names.reduce(function(previous, current) {
let distance = levenshtein(current, name);
if (distance < lowest) {
lowest = distance;
return current;
}
return previous;
}, '');
}
Finally moving on to the actual proxy object, proxies are defined as objects with a target object and a handler object. The target is the object which is virtualized by the proxy and the handler is an object whose properties are traps, or functions that define the behaviour of a proxy when an operation is done to it.
So to make an object’s properties be “autocorrected” we’ll define a function that takes the target as a parameter and returns a proxy which re-defines the get trap:
function autoCorrect(target, recursive) {
return new Proxy(target, {
get: function(target, name) {
if (!(name in target)) {
name = getClosestPropertyName(Object.getOwnPropertyNames(target), name);
}
return target[name];
},
});
}
Which, when in use, would yield the following:
Math = autoCorrect(Math);
console.log(Math.PI); // 3.141592653589793
console.log(Math.PIE); // 3.141592653589793
console.log(Math.PIEE); // 3.141592653589793
Get traps also override the subscript operator because the member and subscript operators use this trap, meaning the following is equivalent to the above example:
Math = autoCorrect(Math);
console.log(Math["PI"]); // 3.141592653589793
console.log(Math["PIE"]); // 3.141592653589793
console.log(Math["PIEE"]); // 3.141592653589793
Strictly typed objects
A slightly more useful variation of the previous use case would be to disallow unknown properties to be used and instead throw an error pointing out the “most likely” candidate.
We’ll re-use the same Levenshtein function as before, but instead of adding a factory function to create the proxy we’ll bake it into the class constructor by returning a proxy to the constructed object instead of the object itself:
class Person {
constructor() {
this.age = '';
return new Proxy(this, {
get: function(target, name) {
if (!(name in target)) {
let alt = getClosestPropertyName(Object.getOwnPropertyNames(target), name);
throw new ReferenceError(`${name} is not defined, did you mean ${alt}?`);
}
return target[name];
},
set: function(target, name, value) {
if (!(name in target)) {
let alt = getClosestPropertyName(Object.getOwnPropertyNames(target), name);
throw new ReferenceError(`${name} is not defined, did you mean ${alt}?`);
}
target[name] = value;
},
});
}
}
Which, would yield the following error when a non-existing property is accessed:
p = new Person();
p.age = 30;
p.name = "Luke"
p.jedi = true; // ReferenceError: jedi is not defined, did you mean age?
Conclusion
Proxies are incredibly powerful and can be used and abused for a wide array of things, but it’s important to remember that proxies cannot be emulated by a pre-processor and have to be supported by the runtime itself. It’s a rare case for a feature introduced that is not backwards compatible. In most cases, we can achieve the same without proxies although it might involve a bit more boilerplate code.
Another thing to keep in mind is that using proxies isn’t free, there is a non-trivial overhead as there is another level of indirection in play. So in some cases, compile-time metaprogramming might be preferred over doing it at run-time.
Lastly, proxies, while fairly magical, do not necessarily lead to very clean and easily understandable code but they’re worth knowing about as there are certainly a few cases where they may be the best way or even the only way forward.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.
The post Terrible use cases for JavaScript proxies appeared first on LogRocket Blog.
Top comments (0)