Reflect & Proxy
Reflect and Proxy are both standard built-in objects introduced as part of the ES6 spec and are supported in all modern browsers. Broadly speaking, they formalize the concept of metaprogramming in the context of Javascript by combining existing introspection and intercession APIs, and expanding upon them. In this article we’ll explore how these objects work using examples that approximate real-world requirements.
Introduction
Javascript engines have object internal methods like [[GetOwnProperty]]
, [[HasProperty]]
, and
[[Set]]
, some of which were already exposed for reflection in earlier versions of the spec. If you’ve
worked with Javascript before, you’re probably familiar with some of these developer-accessible
equivalents. For example…
|
|
The examples above demonstrate static introspection methods defined on the global Object
. They only
represent a subset of the useful engine-internal methods we’d like to access, and they’re tacked on to a
prototype. Together, the Reflect and Proxy APIs unify and simplify these existing methods, expand upon
their introspection capabilities, and expose intercession APIs that were previously not possible.
Instead of covering every function defined on each of these objects in this article, we’ll focus on the functions we use most often at Reflect. To learn more about each we recommend reading through the MDN guides.
Simple Reflect Example
Let’s imagine a scenario in which you’d like to log some information every time a field on some global
object was accessed. You could find every instance of a get()
call throughout your app and send the
information manually…
|
|
This pattern is flawed for a number of reasons
- It requires proprietary knowledge: Developers are responsible for remembering that every time they
access some field on
globalSession
, they must also include a call toconsole.log()
. This is difficult to enforce and easy to forget. - It does not scale: If the name of a field on
globalSession
changes, refactoring would be a nightmare. If you’d like to implement the same policy for some object other thanglobalSession
, you’d need to repeat the entire original process and further expand upon the proprietary knowledge needed to develop in the codebase - It doesn’t account for more complex scenarios: The example above demonstrates simple access patterns, but what happens when you have something like the following?
|
|
The flaws in the approach above illustrate a disconnect between what we’re trying to express and how we’ve implemented our solution. We want to log some information to the console every time a field on some object is accessed. We’ve solved this by enforcing a rule which requires manually calling a function.
The Proxy
object allows us to solve the problem by expressing the desired behavior rather than trying to
enforce a flimsy policy. Here’s how that would work.
|
|
Every time anyone accesses any field on globalSession
(directly or indirectly), that access will
automatically be logged to the console.
This solves the flaws in the pattern above
- There is no proprietary knowledge needed: Developers can access fields on
globalSession
without remembering to store information about said access - It scales: Refactoring
globalSession
is as easy as refactoring any other object, and the samemakeStoreAccessProxy
function can be used on any object in the entire codebase at any time - It accounts for more complex scenarios: If you
get()
some field onglobalSession
by way of some other object that points to it, the access will still be logged to the console.
Note that we’ve leveraged both the Proxy
and Reflect
APIs in order to achieve the desired
result. We’ll review this piece by piece:
|
|
The consistency between the Proxy’s get()
method in its handler and the Reflect.get
function holds for
all functions on both objects. Every method you can define on a Proxy
handler has an equivalent function
on the Reflect
object. You could create a completely pointless proxy which just acted as a passthrough by
overriding every supported method and simply calling the Reflect
equivalent…
|
|
Advanced Reflect Example
In this case, the code we’re writing needs to keep track of all images on the page that are loaded dynamically by
some web application we do not control. Since we cannot manipulate the underlying application’s code directly, we
need some mechanism by which we’ll trap access to the src
attribute transparently…
|
|
From an application’s perspective, this change is transparent. The src
attribute of any <img>
node can be
manipulated as though this override didn’t exist. We’re only intercepting access to these fields, taking some
action, then carrying on as though nothing happened. The underlying app would require not knowledge of such a
change and would remain functionally unchanged.
Proxy Example
How could we leverage the Proxy
object? We may need to trap behaviors captured deep in the internals of
some library or framework in order to redefine them entirely. Let’s imagine a scenario in which a framework
has two internal methods that manipulate the DOM. Both methods achieve the same end result, but one is asynchronous
while the other is not. The asynchronous version may be the better choice for most apps for performance reasons, but
in order to accurately track every action a user is taking we’d prefer it if developers only used the synchronous version.
With Proxy
, this isn’t a problem, and it’s something we can control entirely ourselves without the need for
applications to change their own source.
|
|
Conclusion
It’s important to be thoughtful when using the APIs described in this article. In general, web applications
should not be redefining core web APIs (we think Reflect’s use-case is an exception), but when Proxy
and Reflect
are the right tools for the job, it’s also important to understand how they work. For instance, in the past we’ve
used the Reflect.defineProperty
function to redefine a global 3rd party property that exists on many sites on
the web, but when we did so we forgot to include the enumerable: true
field. One site in particular was relying
on that property being enumerable, and so when we redefined it some functionality on their site stopped working
in the context of using the Reflect app.
Reflect (the application) can be thought of as a top-to-bottom reflective web application container that ideally is transparent to the web application its observing and manipulating. If you’d like to learn more about how Reflect works, we’d love to hear from you! You can reach us at info@reflect.run. Happy testing!