ponyfoo.com

Practical Modern JavaScript

Dive into ES6 and the future of JavaScript — Modular JavaScript Book Series
O’Reilly Media334 PagesISBN 978-1-4919-4353-3

“I am delighted to support Nicolás’ endeavor because his book looks exactly like what people who are coming to JavaScript with fresh eyes need.”

– Brendan Eich

Ideal for professional software developers with a basic understanding of JavaScript, this practical book shows you how to build small, interconnected ES6 JavaScript modules that emphasize reusability. You’ll learn how to face a project with a modular mindset, and how to organize your applications into simple pieces that work well in isolation and can be combined to create a large, robust application.

This book focuses on two aspects of JavaScript development: modularity and ES6 features. You’ll learn how to tackle application development by following a scale-out approach. As pieces of your codebase grow too big, you can break them up into smaller modules.

The book can be read online for free or purchased through Amazon.

This book is part of the Modular JavaScript series.

🗞 Start with the book series launch announcement on Pony Foo
💳 Participate in the crowdfunding campaign on Indiegogo
🌩 Amplify the announcement on social media via Thunderclap
🐤 Share a message on Twitter or within your social circles
👏 Contribute to the source code repository on GitHub
🦄 Read the free HTML version of the book on Pony Foo
📓 Purchase the book from O’Reilly on Amazon

Chapter 6

Managing Property Access with Proxies

Proxies are an interesting and powerful feature in ES6 that act as intermediaries between API consumers and objects. In a nutshell, you can use a Proxy to determine the desired behavior whenever the properties of an underlying target object are accessed. A handler object can be used to configure traps for your Proxy, which define and restrict how the underlying object is accessed, as we’ll see in a bit.

Getting Started with Proxy

By default, proxies don’t do much—in fact they don’t do anything. If you don’t provide any configuration, your proxy will just work as a pass-through to the target object, also known as a “no-op forwarding proxy,” meaning that all operations on the proxy object defer to the underlying object.

In the following piece of code, we create a no-op forwarding Proxy. You can observe how by assigning a value to proxy.exposed, that value is passed onto target.exposed. You could think of proxies as the gatekeepers of their underlying objects: they may allow certain operations to go through and prevent others from passing, but they carefully inspect every single interaction with their underlying objects.

const target = {}
const handler = {}
const proxy = new Proxy(target, handler)
proxy.exposed = true
console.log(target.exposed)
// <- true
console.log(proxy.somethingElse)
// <- undefined

We can make the proxy object a bit more interesting by adding traps. Traps allow you to intercept interactions with target in several different ways, as long as those interactions happen through the proxy object. For instance, we could use a get trap to log every attempt to pull a value out of a property in target, or a set trap to prevent certain properties from being written to. Let’s kick things off by learning more about get traps.

Trapping get Accessors

The proxy in the following code listing is able to track any and every property access event because it has a handler.get trap. It can also be used to transform the value returned by accessing any given property before returning a value to the accessor.

const handler = {
  get(target, key) {
    console.log(`Get on property "${ key }"`)
    return target[key]
  }
}
const target = {}
const proxy = new Proxy(target, handler)
proxy.numbers = [1, 1, 2, 3, 5, 8, 13]
proxy.numbers
// 'Get on property "numbers"'
// <- [1, 1, 2, 3, 5, 8, 13]
proxy['something-else']
// 'Get on property "something-else"'
// <- undefined

As a complement to proxies, ES6 introduces a Reflect built-in object. The traps in ES6 proxies are mapped one-to-one to the Reflect API: for every trap, there’s a matching reflection method in Reflect. These methods can be particularly useful when we want the default behavior of proxy traps, but we don’t want to concern ourselves with the implementation of that behavior.

In the following code snippet we use Reflect.get to provide the default behavior for get operations, while not worrying about accessing the key property in target by hand. While in this case the operation may seem trivial, the default behavior for other traps may be harder to remember and implement correctly. We can forward every parameter in the trap to the reflection API and return its result.

const handler = {
  get(target, key) {
    console.log(`Get on property "${ key }"`)
    return Reflect.get(target, key)
  }
}
const target = {}
const proxy = new Proxy(target, handler)

The get trap doesn’t necessarily have to return the original target[key] value. Imagine the case where you wanted properties prefixed by an underscore to be inaccessible. In this case, you could throw an error, letting the consumer know that the property is inaccessible through the proxy.

const handler = {
  get(target, key) {
    if (key.startsWith('_')) {
      throw new Error(`Property "${ key }" is inaccessible.`)
    }
    return Reflect.get(target, key)
  }
}
const target = {}
const proxy = new Proxy(target, handler)
proxy._secret
// <- Uncaught Error: Property "_secret" is inaccessible.

To the keen observer, it may be apparent that disallowing access to certain properties through the proxy becomes most useful when creating a proxy with clearly defined access rules for the underlying target object, and exposing that proxy instead of the target object. That way you can still access the underlying object freely, but consumers are forced to go through the proxy and play by its rules, putting you in control of exactly how they can interact with the object. This wasn’t possible before proxies were introduced in in ES6.

Trapping set Accessors

As the in counterpart of get traps, set traps can intercept property assignment. Suppose we wanted to prevent assignment on properties starting with an underscore. We could replicate the get trap we implemented earlier to block assignment as well.

The Proxy in the next example prevents underscored property access for both get and set when accessing target through proxy. Note how the set trap returns true here? Returning true in a set trap means that setting the property key to the provided value should succeed. If the return value for the set trap is false, setting the property value will throw a TypeError under strict mode, and otherwise fail silently. If we were using Reflect.set instead, as brought up earlier, we wouldn’t need to concern ourselves with these implementation details: we could just return Reflect.set(target, key, value). That way, when somebody reads our code later, they’ll be able to understand that we’re using Reflect.set, which is equivalent to the default operation, equivalent to the case where a Proxy object isn’t part of the equation.

const handler = {
  get(target, key) {
    invariant(key, 'get')
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    invariant(key, 'set')
    return Reflect.set(target, key, value)
  }
}
function invariant(key, action) {
  if (key.startsWith('_')) {
    throw new Error(`Can't ${ action } private "${ key }"
    property`)
  }
}
const target = {}
const proxy = new Proxy(target, handler)

The following piece of code demonstrates how the proxy responds to consumer interaction.

proxy.text = 'the great black pony ate your lunch'
console.log(target.text)
// <- 'the great black pony ate your lunch'
proxy._secret
// <- Error: Can't get private "_secret" property
proxy._secret = 'invalidate'
// <- Error: Can't set private "_secret" property

The object being proxied, target in our latest example, should be completely hidden from consumers, so that they are forced to access it exclusively through proxy. Preventing direct access to the target object means that they will have to obey the access rules defined on the proxy object—such as “properties prefixed with an underscore are off-limits.”

To that end, you could wrap the proxied object in a function and then return the proxy.

function proxied() {
  const target = {}
  const handler = {
    get(target, key) {
      invariant(key, 'get')
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      invariant(key, 'set')
      return Reflect.set(target, key, value)
    }
  }
  return new Proxy(target, handler)
}
function invariant(key, action) {
  if (key.startsWith('_')) {
    throw new Error(`Can't ${ action } private "${ key }"
    property`)
  }
}

Usage stays the same, except that now access to target is completely governed by proxy and its mischievous traps. At this point, any _secret properties in target are completely inaccessible through the proxy, and since target can’t be accessed directly from outside the proxied function, they’re sealed off from consumers for good.

A general-purpose approach would be to offer a proxying function that takes an original object and returns a proxy. You can then call that function whenever you’re about to expose a public API, as shown in the following code block. The concealWithPrefix function wraps the original object in a Proxy where properties prefixed with a prefix value (or _ if none is provided) can’t be accessed.

function concealWithPrefix(original, prefix='_') {
  const handler = {
    get(original, key) {
      invariant(key, 'get')
      return Reflect.get(original, key)
    },
    set(original, key, value) {
      invariant(key, 'set')
      return Reflect.set(original, key, value)
    }
  }
  return new Proxy(original, handler)
  function invariant(key, action) {
    if (key.startsWith(prefix)) {
      throw new Error(`Can't ${ action } private "${ key }"
      property`)
    }
  }
}
const target = {
  _secret: 'secret',
  text: 'everyone-can-read-this'
}
const proxy = concealWithPrefix(target)
// expose proxy to consumers

You might be tempted to argue that you could achieve the same behavior in ES5 simply by using variables privately scoped to the concealWithPrefix function, without the need for the Proxy itself. The difference is that proxies allow you to “privatize” property access dynamically. Without relying on Proxy, you couldn’t mark every property that starts with an underscore as private. You could use Object.freeze1 on the object, but then you wouldn’t be able to modify the property references yourself, either. Or you could define get and set accessors for every property, but then again you wouldn’t be able to block access on every single property, only the ones you explicitly configured getters and in setters for.

1
The Object.freeze method prevents adding new properties, removing existing ones, and modifying property value references. Note that it doesn’t make the values themselves immutable: their properties can still change, provided Object.freeze isn’t called on those objects as well.
Unlock with one Tweet!
Grants you full online access to Practical Modern JavaScript!
You can also read the book on the public git repository, but it won’t be as pretty! 😅