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 outProxy
. 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 beyondget
andset
.
« 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)
Yoni 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
}
}