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 even more Proxy
traps now! If you haven’t yet, I encourage you to read the previous article on the Proxy
built-in for an introduction to the subject and the one about the first few traps I covered.
But Wait! There’s More… (Proxy Trap Handlers)
This article covers all the trap handlers that weren’t covered by the two previous articles on proxies. For the most part, the traps that we discussed yesterday had to do with property manipulation, while the first five traps we’ll dig into today have mostly to do with the object being proxied itself. The last two have to do with properties once again – but they’re a bit more involved than yesterday’s traps, which were much easier to “fall into” (the trap – muahaha) in your everyday code.
construct
– traps usage of thenew
operatorgetPrototypeOf
– traps internal calls to[[GetPrototypeOf]]
setPrototypeOf
– traps calls toObject.setPrototypeOf
isExtensible
– traps calls toObject.isExtensible
preventExtensions
– traps calls toObject.preventExtensions
getOwnPropertyDescriptor
– traps calls toObject.getOwnPropertyDescriptor
construct
You can use the handler.construct
method to trap usage of the new
operator. Here’s a quick “default implementation” that doesn’t alter the behavior of new
at all. Remember our friend the spread operator?
var handler = {
construct (target, args) {
return new target(...args)
}
}
If you use the handler
options above, the new
behavior you’re already used to would remain unchanged. That’s great because it means whatever you’re trying to accomplish you can still fall back to the default behavior – and that’s always important.
function target (a, b, c) {
this.a = a
this.b = b
this.c = c
}
var proxy = new Proxy(target, handler)
console.log(new proxy(1,2,3))
// <- { a: 1, b: 2, c: 3 }
Obvious use cases for construct
traps include data massaging in the arguments, doing things that should always be done around a call to new proxy()
, logging and tracking object creation, and swapping implementations entirely. Imagine a proxy like the following in situations where you have inheritance “branching”.
class Automobile {}
class Car extends Automobile {}
class SurveillanceVan extends Automobile {}
class SUV extends Automobile {}
class SportsCar extends Car {}
function target () {}
var handler = {
construct (target, args) {
var [status] = args
if (status === 'nsa') {
return new SurveillanceVan(...args)
}
if (status === 'single') {
return new SportsCar(...args)
}
return new SUV(...args) // family
}
}
Naturally, you could’ve used a regular method for the branching part, but using the new
operator also makes sense in these types of situations, as you’ll end up creating a new object in all code branches anyways.
console.log(new proxy('nsa').constructor.name)
// <- `SurveillanceVan`
The most common use case for construct
traps yet may be something simpler, and that’s extending the target
object right after creation, – and before anything else happens – in such a way that it better supports the proxy
gatekeeper. You might have to add a proxied
flag to the target
object, or something akin to that.
getPrototypeOf
You can use the handler.getPrototypeOf
method as a trap for all of the following.
Object.prototype.__proto__
propertyObject.prototype.isPrototypeOf()
methodObject.getPrototypeOf()
methodReflect.getPrototypeOf()
methodinstanceof
operator
You could use this trap to make an object pretend it’s an Array
, when accessed through the proxy. However, note that that on its own isn’t sufficient for the proxy
to be an actual Array
.
var handler = {
getPrototypeOf: target => Array.prototype
}
var target = {}
var proxy = new Proxy(target, handler)
console.log(proxy instanceof Array)
// <- true
console.log(proxy.push)
// <- undefined
Naturally, you could keep on patching your proxy
until you get the behavior you want. In this case, you may want to use a get
trap to mix the Array.prototype
with the actual back-end target
. Whenever a property isn’t found on the target
, we’ll use reflection to look it up on Array.prototype
. It turns out, this is good enough for most operations.
var handler = {
getPrototypeOf: target => Array.prototype,
get (target, key) {
return Reflect.get(target, key) || Reflect.get(Array.prototype, key)
}
}
var target = {}
var proxy = new Proxy(target, handler)
console.log(proxy.push)
// <- function push () { [native code] }
proxy.push('a', 'b')
console.log(proxy)
// <- { 0: 'a', 1: 'b', length: 2 }
I definitely see some advanced use cases for getPrototypeOf
traps in the future, but it’s too early to tell what patterns may come out of it.
setPrototypeOf
The Object.setPrototypeOf
method does exactly what its name conveys: it sets the prototype of an object to a reference to another object. It’s considered the proper way of setting the prototype as opposed to using __proto__
which is a legacy feature – and now standarized as such.
You can use the handler.setPrototypeOf
method to set up a trap for Object.setPrototypeOf
. The snippet of code shown below doesn’t alter the default behavior of changing a prototype to the value of proto
.
var handler = {
setPrototypeOf (target, proto) {
Object.setPrototypeOf(target, proto)
}
}
var proto = {}
var target = function () {}
var proxy = new Proxy(target, handler)
proxy.setPrototypeOf(proxy, proto)
console.log(proxy.prototype === proto)
// <- true
The field for setPrototypeOf
is pretty open. You could simply not call Object.setPrototypeOf
and the trap would sink the call into a no-op. You could throw
an exception making the failure more explicit – for instance if you deem the new prototype to be invalid or you don’t want consumers pulling the rug from under your feet.
This is a nice trap to have if you want proxies to have limited access to what they can do with your target
object. I would definitely implement a trap like the one below if I had any security concerns at all in a proxy I’m passing away to third party code.
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden')
}
}
var proto = {}
var target = function () {}
var proxy = new Proxy(target, handler)
proxy.setPrototypeOf(proxy, proto)
// <- Error: Changing the prototype is forbidden
Then again, you may want to fail silently with no error being thrown at all if you’d rather confuse the consumer – and that may just make them go away.
isExtensible
The handler.isExtensible
method can be mostly used for logging or auditing calls to Object.isExtensible
. This trap is subject to a harsh invariant that puts a hard limit to what you can do with it.
If
Object.isExtensible(proxy) !== Object.isExtensible(target)
, then aTypeError
is thrown.
You could use the handler.isExtensible
trap to throw
if you don’t want consumers to know whether the original object is extensible or not, but there seem to be limited situations that would warrant such an incarnation of evil. For completeness’ sake, the piece of code below shows a trap for isExtensible
that throws errors every once in a while, but otherwise behaves as expected.
var handler = {
isExtensible (target) {
if (Math.random() > 0.1) {
throw new Error('gotta love sporadic obscure errors!')
}
return Object.isExtensible(target)
}
}
var target = {}
var proxy = new Proxy(target, handler)
console.log(Object.isExtensible(proxy))
// <- true
console.log(Object.isExtensible(proxy))
// <- true
console.log(Object.isExtensible(proxy))
// <- true
console.log(Object.isExtensible(proxy))
// <- Error: gotta love sporadic obscure errors!
While this trap is nearly useless other than for auditing purposes and to cover all your bases, the hard-to-break invariant makes sense because there’s also the preventExtensions
trap. That one is a little bit more useful!
preventExtensions
You can use handler.preventExtensions
to trap the Object.preventExtensions
method. When extensions are prevented on an object, new properties can’t be added any longer – it can’t be extended.
Imagine a scenario where you want to selectively be able to preventExtensions
on some objects – but not all of them. In this scenario, you could use a WeakSet
to keep track of the objects that should be extensible. If an object is in the set, then the preventExtensions
trap should be able to capture those requests and discard them. The snippet below does exactly that. Note that the trap always returns the opposite of Object.isExtensible(target)
, because it should report whether the object has been made non-extensible.
var mustExtend = new WeakSet()
var handler = {
preventExtensions (target) {
if (!mustExtend.has(target)) {
Object.preventExtensions(target)
}
return !Object.isExtensible(target)
}
}
Now that we’ve set up the handler
and our WeakSet
, we can create a back-end object, a proxy
, and add the back-end to our set. Then, you can try Object.preventExtensions
on the proxy and you’ll notice it fails to prevent extensions to the object. This is the intended behavior as the target
can be found in the mustExtend
set.
var target = {}
var proxy = new Proxy(target, handler)
mustExtend.add(target)
Object.preventExtensions(proxy)
// <- TypeError: proxy preventExtensions handler returned false
If we removed the target
from the mustExtend
set before calling Object.preventExtensions
, then target
would be made non-extensible as originally intended.
var target = {}
var proxy = new Proxy(target, handler)
mustExtend.add(target)
mustExtend.delete(target)
Object.preventExtensions(proxy)
console.log(Object.isExtensible(proxy))
// <- false
Naturally, you could use this distinction to prevent proxy
consumers from making the proxy non-extensible in cases where that could lead to undesired behavior. In most cases, you probably won’t have to deal with this trap, though. That’s because you’re usually going to be working with the back-end target
for the most part, and not so much with the proxy
object itself.
getOwnPropertyDescriptor
You can use the handler.getOwnPropertyDescriptor
method as a trap for Object.getOwnPropertyDescriptor
. It may return a property descriptor, such as the result from Object.getOwnPropertyDescriptor(target, key)
; or undefined
, signaling that the property doesn’t exist. As usual, you also have the third option of throwing an exception, aborting the operation entirely.
If we go back to our canonical “private property space” example, we could implement a trap such as the one seen below to prevent consumers from learning about property descriptors of private properties.
var handler = {
getOwnPropertyDescriptor (target, key) {
invariant(key, 'get property descriptor for')
return Object.getOwnPropertyDescriptor(target, key)
}
}
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)
Object.getOwnPropertyDescriptor(proxy, '_foo')
// <- Error: Invalid attempt to get property descriptor for private "_foo" property
The problem with that approach is that you’re effectively telling consumers that properties with the _
prefix are somehow off-limits. It might be best to conceal them entirely by returning undefined
. This way, your private properties will behave no differently than properties that are actually absent from the target
object.
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return
}
return Object.getOwnPropertyDescriptor(target, key)
}
}
var target = { _foo: 'bar', baz: 'tar' }
var proxy = new Proxy(target, handler)
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'))
// <- undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'))
// <- undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'))
// <- { value: 'tar', writable: true, enumerable: true, configurable: true }
Usually when you’re trying to hide things it’s best to have them try and behave as if they fell in some other category than the category they’re actually in. Throwing, however, just sends the “there’s something sketchy here, but we can’t quite tell you what that is…” message – and the consumer will eventually find out why that is.
Keep in mind that if debugging concerns outweight security concerns, you probably should go for the
throw
statement.
Conclusions
This has certainly been fun. I now have a much better understanding of what proxies can do for me, and I think they’ll be an instant hit once ES6 starts gaining more traction. I for one can’t be more excited about them becoming well-supported in more browsers soon. I wouldn’t hold out my hopes about proxies for Babel, as many of the traps are ridiculously hard (or downright impossible) to implement in ES5.
As we’ve learned over the last few days, there’s a myriad use cases for proxies. Off the top of my head, we can use Proxy
for all of the following.
- Add validation rules – and enforce them – on plain old JavaScript objects
- Keep track of every interaction that goes through a proxy
- Decorate objects without changing them at all
- Make certain properties on an object completely invisible to the consumer of a proxy
- Revoke access at will when the consumer should no longer be able to use a proxy
- Modify the arguments passed to a proxied method
- Modify the result produced by a proxied method
- Prevent deletion of specific properties through the proxy
- Prevent new definitions from succeeding, according to the desired property descriptor
- Shuffle arguments around in a constructor
- Return a result other than the object being
new
-ed up in a constructor - Swap out the prototype of an object for something else
I can say without a shadow of a doubt that there’s hundreds more of use cases for proxies. I’m sure many libraries will adopt a pattern we’ve discussed here in the series where a “back-end” target
object is created and used for storage purposes but the consumer is only provided with a “front-end” proxy
object with limited and audited interaction with the back-end.
What would you use
Proxy
for?
Meet me tomorrow at… say – same time? We can talk about Reflect
then.
Comments