A practical guide to Javascript Proxy

tombarr

Thomas Barrasso

Posted on May 16, 2019

A practical guide to Javascript Proxy

Introduction to Proxy

With Proxy, you get a tiger object disguised as a cat object. Here are about half a dozen examples that I hope will convince you that Proxy provides powerful metaprogramming in Javascript.

Although it is not as well known as other ES2015 features, Proxy has many uses including operator overloading, object mocking, concise-yet-flexible API creation, Object on-change events, and even powers the internal reactivity system behind Vue.js 3.

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). – MDN

A Proxy is a placeholder object that “traps” invocations and operations made to its target object which it can then passthrough, no-op, or handle more elegantly. It creates an undetectable barrier around the target object that redirects all operations to the handler object.

A Proxy is created using the new Proxy constructor which accepts two, required arguments: the target object and handler object.

The simplest example of a functioning Proxy is one with a single trap, in this case, a get trap that always returns “42”.

The result is an object that will return “42” for any property access operation. That means target.x, target['x'], Reflect.get(target, 'x'), etc.

However, Proxy traps are certainly not limited to get operations. It is only one of more than a dozen different traps.

Proxy Use Cases

It may not be immediately apparent how such a simple pattern could be so widely used, but hopefully, after a few examples, it will become more clear.

Default/ “Zero Values”

In GoLang, there is the concept of zero values which are type-specific, implicit default struct values. The idea is to provide type-safe default primitives values, or in Gopher-speak, “give your structs a useful zero value!”

Though different creation patterns enable similar functionality, Javascript had no way to wrap an object with implicit initial values. The default value for an unset property in Javascript is undefined. That is, until Proxy.

This three-line function wraps a target object. If the property is set, it returns the property value (pass-through). Otherwise, it returns a default “zero value.” Technically, this approach isn’t implicit either but it could be if we extended withZeroValue to support type-specific (rather than parameterized) zero values for Boolean (false), Number (0), String (“”), Object ({}), Array ([]), etc.

One place where this functionality might be useful is a coordinate system. Plotting libraries may automatically support 2D and 3D rendering based on the shape of the data. Rather than create two separate models, it might make sense to always include z defaulted to zero rather than undefined.

Negative Array Indices

Getting the last element in an Array in Javascript is verbose, repetitive, and prone to off-by-one errors. That’s why there is a TC39 proposal that defines a convenience property, Array.lastItem, to get and set the last element.

Other languages like Python and Ruby make access to terminal elements easier with negative array indices. For example, the last element can be accessed simply with arr[-1] instead of arr[arr.length-1].

With Proxy, negative indices can also be used in Javascript.

One important note is that traps including handler.get stringify all properties. For array access, we need to coerce property names into Numbers which can be done concisely with the unary plus operator.

Now [-1] accesses the last element, [-2] the second to last, and so on.

There is even an npm package, negative-array, that encapsulates this functionality more completely.

Hiding Properties

Javascript has notoriously lacked private properties. Symbol was originally introduced to enable private properties, but later watered down with reflective methods like Object.getOwnPropertySymbols that made them publicly discoverable.

The longstanding convention has been to name private properties with a leading underscore, effectively marking them “do not touch.” Proxy offers a slightly better approach to masking such properties.

The hide function wraps a target object and makes properties that are prefixed with an underscore inaccessible from the in operator and from methods like Object.getOwnPropertyNames.

A more complete implementation would also include traps like deleteProperty and defineProperty. Apart from closures, this is probably the approach that is closest to truly-private properties as they are inaccessible from enumeration, cloning, access, or modification.

Caching

There are two hard problems in computer science: cache invalidation, naming things, and off-by-one errors.
It is not uncommon to face difficulties synchronizing state between the client and server. Data can change over time, and it can be difficult to know exactly where to place the logic of when to re-sync.

Proxy enables a new approach: wrap objects to invalidate (and resync) properties as necessary. All attempts to access a property first check against a cacheing strategy that decides to returns what’s currently in memory, or to take some other action.

This function is oversimplified: it renders all properties on an object inaccessible after a certain period of time. However, it would not be difficult to extend this approach to set time-to-live (TTL) on a per-property basis and to update it after a a certain duration or number of accesses.

This example simply renders the bank account balance inaccessible after 10 second. For more in-depth, real-world use cases there are several articles on Caching & Logging and Client-Side Caching using Proxy and sessionStorage.

Enums & Read-Only Views

These examples come from Csaba Hellinger’s article on Proxy Use Cases and Mozilla Hacks. The approach is to wrap an object to prevent extension or modification. Although Object.freeze now provides functionality to render an object read-only, it’s possible to extend this approach for better enum objects that throw errors accessing non-existent properties.

Read-Only View

Enum View

Now we can create an Object that throws an exception if you try to access non-exist properties, rather than returning undefined. This makes it easier to catch and address issues early on.

Our enum example is also the first example of proxies on proxies, confirming that a proxy is a valid target object for another proxy. This facilitates code reuse through composition of Proxy functionality.

This approach can be further extended to include “simulated methods” like nameOf that return the property name given an enum value, mimicking the behavior in languages like Javascript.

While other frameworks and language supersets like TypeScript offer an enum type, this solution is unique in that it works with vanilla Javascript without special build tools or transpilers.

Operator Overload

Perhaps the most fascinating Proxy use case syntactically is the ability to overload operators, like the in operator using handler.has.

The in operator is designed to check if a “specified property is in the specified object or its prototype chain.” But it is also the most syntactically-elegant operator to overload. This example defines a continuous range function to compare Numbers against.

Unlike Python, which uses generators to compare against a finite sequence of whole numbers, this approach supports decimal comparison and could be extended to support other numeric ranges–inclusive, exclusive, natural, rational, imaginary, ad infinitum.

Even though this use case does not solve a complex problem, it does provide clean, readable, and reusable code. 🔥

In addition to the in operator, we can also overload delete and new.

Cookies Object

If you have ever had to interface with cookies in Javascript, you have had to deal with document.cookies. It is an unusual API in that the API is a String that reads out all cookies, semi-colon delimited but you use the assignment operator to initialize or overwrite a single cookie.

document.cookies is a String that looks like:

_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1

In short, dealing with document.cookies is frustrating and error prone. One approach is the simple cookie framework, which can be adapted to use Proxy.

This function returns an object that acts like any other key-value object, but proxies all changes to document.cookies for persistence.

In 11 lines, we have a better interface for modifying cookies, although additional features like string normalization would be necessary in a production environment.

Fine Print

The devil is in the detail, and Proxy is no exception.

Polyfill

At the time of writing (May 2019), there is no complete polyfill for Proxy. There is, however, a partial polyfill for Proxy written by Google that supports the get, set, apply, and construct traps and works for IE9+.

Is it a Proxy?

It is impossible to determine whether an object is a proxy or not. – 2ality

According to the Javascript language specifications, there is no way to determine if an Object is a Proxy. However, on Node 10+ it is possible using the util.types.isProxy method.

What’s the target?

Given a Proxy object, it’s not possible to obtain or change the target object. It is also not possible to obtain or modify the handler object.

The closest approximation is in Ben Nadel’s article Using Proxy to Dynamically Change THIS Binding, which uses an empty object as the Proxy target and closures to cleverly reassign the object the Proxy’s actions are performed on.

Proxy Primitives

Unfortunately, one limitation of Proxy is that the target has to be an Object. That means we cannot use primitives like String directly. 😞

Performance

A major drawback of Proxy is performance. Impact will vary based on browser and use, but Proxy isn’t the best approach for performance-critical code. Of course you can always measure the impact and decide if the advantages of Proxy outweigh the affect on performance.

Why Proxy?

Proxy provides a virtualized interface to control the behavior of any target Object. In doing so, it strikes a balance between simplicity and utility without sacrificing compatibility. Any code that expects an Object can accept a Proxy.

Perhaps the most compelling reason to use Proxy is that many of the examples above are just a few lines long and can easily be composed to create complex functionality. For one final example, we can compose functions from several use cases to create a read-only cookie object that returns a default value for non-existent or “private”, hidden cookies.

I hope these examples have shown that Proxy is more than just an esoteric feature for niche metaprogramming in Javascript.

Resources

💖 💪 🙅 🚩
tombarr
Thomas Barrasso

Posted on May 16, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related