ES6 Proxies in Depth

ES6 Proxies in Depth

Cheers, please come in. This is ES6 – “Elaine, you gotta have a baby!” – in Depth. What? Never heard of it? Check out A Brief History of ES6 Tooling. Then, make your way through destructuring, template literals, arrow functions, the spread operator and rest parameters, improvements coming to object literals, the new classes sugar on top of prototypes, let, const, and the “Temporal Dead Zone”, iterators, generators, Symbols, Maps, WeakMaps, Sets, and WeakSets. We’ll be discussing ES6 proxies today.

Like I did in previous articles on the series, I would love to point out that you should probably set up Babel and follow along the examples with either a REPL or the babel-node CLI and a file. That’ll make it so much easier for you to internalize the concepts discussed in the series. If you aren’t the “install things on my computer” kind of human, you might prefer to hop on CodePen and then click on the gear icon for JavaScript – they have a Babel preprocessor which makes trying out ES6 a breeze. Another alternative that’s also quite useful is to use Babel’s online REPL – it’ll show you compiled ES5 code to the right of your ES6 code for quick comparison.

Note that Proxy is harder to play around with as Babel doesn’t support it unless the underlying browser has support for it. You can check out the ES6 compatibility table for supporting browsers. At the time of this writing, you can use Microsoft Edge or Mozilla Firefox to try out Proxy. Personally, I’ll be verifying my examples using Firefox.

Before getting into it, let me shamelessly ask for your support if you’re enjoying my ES6 in Depth series. Your contributions will go towards helping me keep up with the schedule, server bills, keeping me fed, and maintaining Pony Foo as a veritable source of JavaScript goodies.

Thanks for reading that, and let’s go into Proxies now!

ES6 Proxies

Proxies are a quite interesting feature coming in ES6. In a nutshell, you can use a Proxy to determine behavior whenever the properties of a target object are accessed. A handler object can be used to configure traps for your Proxy, as we’ll see in a bit.

By default, proxies don’t do much – in fact they don’t do anything. If you don’t set any “options”, your proxy will just work as a pass-through to the target object – MDN calls this a "no-op forwarding Proxy", which makes sense.

var target = {}
var handler = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
console.log(target.a)
// <- 'b'
console.log(proxy.c === undefined)
// <- true

We can make our proxy a bit more interesting by adding traps. Traps allow you to intercept interactions with target in different ways, as long as those interactions happen through proxy. We could use a get trap to log every attempt to pull a value out of a property in target. Let’s try that next.

get

The proxy below 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 we get out of accessing any given property. We can already imagine Proxy becoming a staple when it comes to developer tooling.

var handler = {
  get (target, key) {
    console.info(`Get on property "${key}"`)
    return target[key]
  }
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
proxy.a
// <- 'Get on property "a"'
proxy.b
// <- 'Get on property "b"'

Of course, your getter doesn’t necessarily have to return the original target[key] value. How about finally making those _prop properties actually private?

set

Know how we usually define conventions such as Angular’s dollar signs where properties prefixed by a single dollar sign should hardly be accessed from an application and properties prefixed by two dollar signs should not be accessed at all? We usually do something like that ourselves in our applications, typically in the form of underscore-prefixed variables.

The Proxy in the example below prevents property access for both get and set (via a handler.set trap) while accessing target through proxy. Note how set always returns true here? – this means that setting the property key to a given 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.

var handler = {
  get (target, key) {
    invariant(key, 'get')
    return target[key]
  },
  set (target, key, value) {
    invariant(key, 'set')
    return true
  }
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`)
  }
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
console.log(proxy.a)
// <- 'b'
proxy._prop
// <- Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// <- Error: Invalid attempt to set private "_prop" property

You do remember string interpolation with template literals, right?

It might be worth mentioning that the target object (the object being proxied) should often be completely hidden from accessors in proxying scenarios. Effectively preventing direct access to the target and instead forcing access to target exclusively through proxy. Consumers of proxy will get to access target through the Proxy object, but will have to obey your access rules – such as “properties prefixed with _ are off-limits”.

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

function proxied () {
  var target = {}
  var handler = {
    get (target, key) {
      invariant(key, 'get')
      return target[key]
    },
    set (target, key, value) {
      invariant(key, 'set')
      return true
    }
  }
  return new Proxy(target, handler)
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`)
  }
}

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

You might be tempted to argue that you could achieve the same behavior in ES5 simply by using variables privately scoped to the proxied method, without the need for the Proxy itself. The big difference is that proxies allow you to “privatize” property access on different layers. Imagine an underlying underling object that already has several “private” properties, which you still access in some other middletier module that has intimate knowledge of the internals of underling. The middletier module could return a proxied version of underling without having to map the API onto an entirely new object in order to protect those internal variables. Just locking access to any of the “private” properties would suffice!

Here’s a use case on schema validation using proxies.

Schema Validation with Proxies

While, yes, you could set up schema validation on the target object itself, doing it on a Proxy means that you separate the validation concerns from the target object, which will go on to live as a POJO (Plain Old JavaScript Object) happily ever after. Similarly, you can use the proxy as an intermediary for access to many different objects that conform to a schema, without having to rely on prototypal inheritance or ES6 class classes.

In the example below, person is a plain model object, and we’ve also defined a validator object with a set trap that will be used as the handler for a proxy validator of people models. As long as the person properties are set through proxy, the model invariants will be satisfied according to our validation rules.

var validator = {
  set (target, key, value) {
    if (key === 'age') {
      if (typeof value !== 'number' || Number.isNaN(value)) {
        throw new TypeError('Age must be a number')
      }
      if (value <= 0) {
        throw new TypeError('Age must be a positive number')
      }
    }
    return true
  }
}
var person = { age: 27 }
var proxy = new Proxy(person, validator)
proxy.age = 'foo'
// <- TypeError: Age must be a number
proxy.age = NaN
// <- TypeError: Age must be a number
proxy.age = 0
// <- TypeError: Age must be a positive number
proxy.age = 28
console.log(person.age)
// <- 28

There’s also a particularly “severe” type of proxies that allows us to completely shut off access to target whenever we deem it necessary.

Revocable Proxies

We can use Proxy.revocable in a similar way to Proxy. The main differences are that the return value will be { proxy, revoke }, and that once revoke is called the proxy will throw on any operation. Let’s go back to our pass-through Proxy example and make it revocable. Note that we’re not using the new operator here. Calling revoke() over and over has no effect.

var target = {}
var handler = {}
var {proxy, revoke} = Proxy.revocable(target, handler)
proxy.a = 'b'
console.log(proxy.a)
// <- 'b'
revoke()
revoke()
revoke()
console.log(proxy.a)
// <- TypeError: illegal operation attempted on a revoked proxy

This type of Proxy is particularly useful because you can now completely cut off access to the proxy granted to a consumer. You start by passing of a revocable Proxy and keeping around the revoke method (hey, maybe you can use a WeakMap for that), and when its clear that the consumer shouldn’t have access to target anymore, – not even through proxy – you .revoke() the hell out of their access. Goodbye consumer!

Furthermore, since revoke is available on the same scope where your handler traps live, you could set up extremely paranoid rules such as “if a consumer attempts to access a private property more than once, revoke their proxy entirely”.

Check back tomorrow for the second part of the article about proxies, which discusses Proxy traps beyond get and set.

« ES6 WeakMaps, Sets, and WeakSets in DepthES6 Proxy Traps in Depth »

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.

One-click unsubscribe, anytime. Learn more.

Comments (5)

imgYoni Jah wrote on September 21, 2015

Notice that your setters in the examples aren’t actually setting any data and if you actually run them most of the console.log(proxy[key]) will be undefined.

You should set the property on the target before returning from the setters

var validator = {
  set (target, key, value) {
    if (key === 'age') {
      if (typeof value !== 'number' || Number.isNaN(value)) {
        throw new TypeError('Age must be a number')
      }
      if (value <= 0) {
        throw new TypeError('Age must be a positive number')
      }
    }
    target[key] = value  
    return true
  }
}
posted @ 2020-01-28 14:20  cnmz  阅读(136)  评论(0编辑  收藏  举报