Reflection at Reflect: The Reflect and Proxy APIs
Tags: javascript, webdev, programming, todayilearned
Published: 2021-10-26 16:34:41


## 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...

```ts
const foo = { firstName: 'SomeFirstName', age: 99 }
Object.defineProperty(foo, 'lastName', { value: 'SomeLastName', enumerable: true })
const bar = Object.keys(foo) // ['firstName', 'age', 'lastName']
const baz = Object.values(foo) // ['SomeFirstName', 99, 'SomeLastName']
Object.hasOwnProperty.call(foo, 'lastName') // true
```

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...

```ts
// app.ts
// On pageload, we fetch the global session
window.globalSession = fetchSession()

// file1.ts
// We've accessed a field on globalSession, and the developer has logged that
const firstName = globalSession.firstName
console.log('GOT FIELD firstName')

// file2.ts
// Same applies here
const lastName = globalSession.lastName
const age = globalSession.age
const firstRelative = globalSession.relatives[0]
console.log('GOT FIELD lastName')
console.log('GOT FIELD age')
console.log('GOT FIELD relatives[0]')
```

This pattern is flawed for a number of reasons

1. It requires proprietary knowledge: Developers are responsible for remembering that every time they access some field on `globalSession`, they must also include a call to `console.log()`. This is difficult to enforce and easy to forget.
2. 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 than `globalSession`, you'd need to repeat the entire original process and further expand upon the proprietary knowledge needed to develop in the codebase.
3. It doesn't account for more complex scenarios: The example above demonstates simple access patterns, but what happens when you have something like the following?

```ts
// file3.ts
// Point another global to the global session
window.activeSession = globalSession

// file4.ts
// Don't forget that activeSession points to the same object as globalSession, you
// still need to call console.log()!
const middleName = activeSession.middleName
```

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.

```ts
// makeStoreAccessProxy.ts
const makeStoreAccessProxy = (obj: Object) => {
  return new Proxy(obj, {
    get(target, key, receiver) {
      console.log(`GOT FIELD ${key}`)
      return Reflect.get(target, key)
    },
  })
}

// app.ts
window.globalSession = makeStoreAccessProxy(fetchSession())
```

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
1. There is no proprietary knowledge needed: Developers can access fields on `globalSession` without remembering to store information about said access.
2. It scales: Refactoring `globalSession` is as easy as refactoring any other object, and the same `makeStoreAccessProxy` function can be used on any object in the entire codebase at any time.
3. It accounts for more complex scenarios: If you `get()` some field on `globalSession` 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:

```ts
const makeStoreAccessProxy = (obj: Object) => {
  // This function returns a proxy of the provided 'obj'. Without defining the second
  // 'handler' argument, this is a transparent passthrough to 'obj' and would behave as
  // though it _were_ the original 'obj'.
  return new Proxy(obj, {
    // We then define a 'get' function in the handler. This means that we're redefining
    // the fundamental get operation on 'obj'
    get(target, key, receiver) {
      // We've redefined 'get' to log information in the console
      console.log(`GOT FIELD ${key}`)
      // And finally, we're calling 'get' on the original unwrapped 'obj'. We could
      // instead return 'target[key]', but this demonstrates the consistency between
      // the Proxy and Reflect APIs
      return Reflect.get(target, key)
    }
  })
}
```

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...

```ts
const p = new Proxy({}, {
  defineProperty() { return Reflect.defineProperty(...arguments) },
  getPrototypeOf() { return Reflect.getPrototypeOf(...arguments) },
  get() { return Reflect.get(...arguments) },
  set() { return Reflect.set(...arguments) },
  ... // etc
})
```

## 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...

```ts
// First we'll store a reference to the original property descriptor for the
// HTMLImageElement's src field
const originalImgSrc = Reflect.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src')

// Then we'll overwrite the HTMLImageElement prototype's "src" property and trap
// calls to that field's get() and set() methods
Reflect.defineProperty(HTMLImageElement.prototype, 'src', {
  get() {
    // When <someImg>.src is called anywhere, we'll log some information, then call the
    // target's get() method also using the Reflect API
    console.log('getting the src')
    return Reflect.apply(originalImgSrc.get, this, [])
  },
  set(value) {
    // When <someImg>.src = 'something' is called anywhere, we'll log some information, then call the
    // target's set() method also using the Reflect API
    console.log(`setting src to ${value}`)
    return Reflect.apply(originalImgSrc.set, this, [value])
  },
})
```

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.

```ts
const someFramework = document.querySelector('#framework-root').framework

someFramework.blockingUpdate = new Proxy(someFramework.blockingUpdate, {
  apply(target, thisArg, argArray) {
    // Here we'll take some action whenever a call to blockingUpdate() is made
    console.log('Intercepted a call to blockingUpdate()')
    Reflect.apply(target, thisArg, argArray)
  },
})

someFramework.asyncUpdate = new Proxy(someFramework.asyncUpdate, {
  apply(target, thisArg, argArray) {
    // Here we'll redefine calls to asyncUpdate() to instead invoke blockingUpdate()
    Reflect.apply(someFramework.blockingUpdate, thisArg, argArray)
  },
})
```

## 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 it is 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!