ponyfoo.com

ES6 Proxy Traps in Depth

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

Welcome to ES6 – “Please, not again” – in Depth. Looking for other ES6 goodness? Refer to 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, and proxies. We’ll be discussing ES6 proxy traps 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 more Proxy traps now! If you haven’t yet, I encourage you to read yesterday’s article on the Proxy built-in for an introduction to the subject.

Proxy Trap Handlers

An interesting aspect of proxies is how you can use them to intercept just about any interaction with a target object – not just get or set operations. Below are some of the traps you can set up, here’s a summary.

We’ll bypass get and set, because we already covered those two yesterday; and there’s a few more traps that aren’t listed here that will make it into an article published tomorrow. Stay tuned!

has

You can use handler.has to “hide” any property you want. It’s a trap for the in operator. In the set trap example we prevented changes and even access to _-prefixed properties, but unwanted accessors could still ping our proxy to figure out whether these properties are actually there or not. Like Goldilocks, we have three options here.

  • We can let key in proxy fall through to key in target
  • We can return false (or true) – even though key may or may not actually be there
  • We can throw an error and deem the question invalid in the first place

The last option is quite harsh, and I imagine it being indeed a valid choice in some situations – but you would be acknowledging that the property (or “property space”) is, in fact, protected. It’s often best to just smoothly indicate that the property is not in the object. Usually, a fall-through case where you just return the result of the key in target expression is a good default case to have.

In our example, we probably want to return false for properties in the _-prefixed “property space” and the default of key in target for all other properties. This will keep our inaccessible properties well hidden from unwanted visitors.

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

Note how accessing properties through the proxy will now return false whenever accessing one of our private properties, with the consumer being none the wiser – completely unaware that we’ve intentionally hid the property from them.

var target = { _prop: 'foo', pony: 'foo' }
var proxy = new Proxy(target, handler)
console.log('pony' in proxy)
// <- true
console.log('_prop' in proxy)
// <- false
console.log('_prop' in target)
// <- true

Sure, we could’ve thrown an exception instead. That’d be useful in situations where attempts to access properties in the private space is seen more of as a mistake that results in broken modularity than as a security concern in code that aims to be embedded into third party websites.

It really depends on your use case!

deleteProperty

I use the delete operator a lot. Setting a property to undefined clears its value, but the property is still part of the object. Using the delete operator on a property with code like delete foo.bar means that the bar property will be forever gone from the foo object.

var foo = { bar: 'baz' }
foo.bar = 'baz'
console.log('bar' in foo)
// <- true
delete foo.bar
console.log('bar' in foo)
// <- false

Remember our set trap example where we prevented access to _-prefixed properties? That code had a problem. Even though you couldn’t change the value of _prop, you could remove the property entirely using the delete operator. Even through the proxy object!

var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
console.log('_prop' in proxy)
// <- true
delete proxy._prop
console.log('_prop' in proxy)
// <- false

You can use handler.deleteProperty to prevent a delete operation from working. Just like with the get and set traps, throwing in the deleteProperty trap will be enough to prevent the deletion of a property.

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

If we run the exact same piece of code we tried earlier, we’ll run into the exception while trying to delete _prop from the proxy.

var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
console.log('_prop' in proxy)
// <- true
delete proxy._prop
// <- Error: Invalid attempt to delete private "_prop" property

Deleting properties in your _private property space is no longer possible for consumers interacting with target through the proxy.

defineProperty

We typically use Object.defineProperty(obj, key, descriptor) in two types of situations.

  1. When we wanted to ensure cross-browser support of getters and setters
  2. Whenever we want to define a custom property accessor

Properties added by hand are read-write, they are deletable, and they are enumerable. Properties added through Object.defineProperty, in contrast, default to being read-only, write-only, non-deletable, and non-enumerable – in other words, the property starts off being completely immutable. You can customize these aspects of the property descriptor, and you can find them below – alongside with their default values when using Object.defineProperty.

  • configurable: false disables most changes to the property descriptor and makes the property undeletable
  • enumerable: false hides the property from for..in loops and Object.keys
  • value: undefined is the initial value for the property
  • writable: false makes the property value immutable
  • get: undefined is a method that acts as the getter for the property
  • set: undefined is a method that receives the new value and updates the property’s value

Note that when defining a property you’ll have to choose between using value and writable or get and set. When choosing the former you’re configuring a data descriptor – this is the kind you get when declaring properties like foo.bar = 'baz', it has a value and it may or may not be writable. When choosing the latter you’re creating an accessor descriptor, which is entirely defined by the methods you can use to get() or set(value) the value for the property.

The code sample below shows how property descriptors are completely different depending on whether you went for the declarative option or through the programmatic API.

var target = {}
target.foo = 'bar'
console.log(Object.getOwnPropertyDescriptor(target, 'foo'))
// <- { value: 'bar', writable: true, enumerable: true, configurable: true }
Object.defineProperty(target, 'baz', { value: 'ponyfoo' })
console.log(Object.getOwnPropertyDescriptor(target, 'baz'))
// <- { value: 'ponyfoo', writable: false, enumerable: false, configurable: false }

Now that we went over a blitzkrieg overview of Object.defineProperty, we can move on to the trap.

It’s a Trap

The handler.defineProperty trap can be used to intercept calls to Object.defineProperty. You get the key and the descriptor being used. The example below completely prevents the addition of properties through the proxy. How cool is it that this intercepts the declarative foo.bar = 'baz' property declaration alternative as well? Quite cool!

var handler = {
  defineProperty (target, key, descriptor) {
    return false
  }
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.foo = 'bar'
// <- TypeError: proxy defineProperty handler returned false for property '"foo"'

If we go back to our “private properties” example, we could use the defineProperty trap to prevent the creation of private properties through the proxy. We’ll reuse the invariant method we had to throw on attempts to define a property in the “private _-prefixed space”, and that’s it.

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

You could then try it out on a target object, setting properties with a _ prefix will now throw an error. You could make it fail silently by returning false – depends on your use case!

var target = {}
var proxy = new Proxy(target, handler)
proxy._foo = 'bar'
// <- Error: Invalid attempt to define private "_foo" property

Your proxy is now safely hiding _private properties behind a trap that guards them from definition through either proxy[key] = value or Object.defineProperty(proxy, key, { value }) – pretty amazing!

enumerate

The handler.enumerate method can be used to trap for..in statements. With has we could prevent key in proxy from returning true for any property in our underscored private space, but what about a for..in loop? Even though our has trap hides the property from a key in proxy check, the consumer will accidentally stumble upon the property when using a for..in loop!

var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false
    }
    return key in target
  }
}
var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
for (let key in proxy) {
  console.log(key)
  // <- '_prop'
}

We can use the enumerate trap to return an iterator that’ll be used instead of the enumerable properties found in proxy during a for..in loop. The returned iterator must conform to the iterator protocol, such as the iterators returned from any Symbol.iterator method. Here’s a possible implementation of such a proxy that would return the output of Object.keys minus the properties found in our private space.

var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false
    }
    return key in target
  },
  enumerate (target) {
    return Object.keys(target).filter(key => key[0] !== '_')[Symbol.iterator]()
  }
}
var target = { pony: 'foo', _bar: 'baz', _prop: 'foo' }
var proxy = new Proxy(target, handler)
for (let key in proxy) {
  console.log(key)
  // <- 'pony'
}

Now your private properties are hidden from those prying for..in eyes!

ownKeys

The handler.ownKeys method may be used to return an Array of properties that will be used as a result for Reflect.ownKeys() – it should include all properties of target (enumerable or not, and symbols too). A default implementation, as seen below, could just call Reflect.ownKeys on the proxied target object. Don’t worry, we’ll get to the Reflect built-in later in the es6-in-depth series.

var handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target)
  }
}

Interception wouldn’t affect the output of Object.keys in this case.

var target = {
  _bar: 'foo',
  _prop: 'bar',
  [Symbol('secret')]: 'baz',
  pony: 'ponyfoo'
}
var proxy = new Proxy(target, handler)
for (let key of Object.keys(proxy)) {
  console.log(key)
  // <- '_bar'
  // <- '_prop'
  // <- 'pony'
}

Do note that the ownKeys interceptor is used during all of the following operations.

  • Object.getOwnPropertyNames() – just non-symbol properties
  • Object.getOwnPropertySymbols() – just symbol properties
  • Object.keys() – just non-symbol enumerable properties
  • Reflect.ownKeys() – we’ll get to Reflect later in the series!

In the use case where we want to shut off access to a property space prefixed by _, we could take the output of Reflect.ownKeys(target) and filter that.

var handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_')
  }
}

If we now used the handler in the snippet above to pull the object keys, we’ll just find the properties in the public, non _-prefixed space.

var target = {
  _bar: 'foo',
  _prop: 'bar',
  [Symbol('secret')]: 'baz',
  pony: 'ponyfoo'
}
var proxy = new Proxy(target, handler)
for (let key of Object.keys(proxy)) {
  console.log(key)
  // <- 'pony'
}

Symbol iteration wouldn’t be affected by this as sym[0] yields undefined – and in any case decidedly not '_'.

var target = {
  _bar: 'foo',
  _prop: 'bar',
  [Symbol('secret')]: 'baz',
  pony: 'ponyfoo'
}
var proxy = new Proxy(target, handler)
for (let key of Object.getOwnPropertySymbols(proxy)) {
  console.log(key)
  // <- Symbol(secret)
}

We were able to hide properties prefixed with _ from key enumeration while leaving symbols and other properties unaffected.

apply

The handler.apply method is quite interesting. You can use it as a trap on any invocation of proxy. All of the following will go through the apply trap for your proxy.

proxy(1, 2)
proxy(...args)
proxy.call(null, 1, 2)
proxy.apply(null, [1, 2])

The apply method takes three arguments.

  • target – the function being proxied
  • ctx – the context passed as this to target when applying a call
  • args – the arguments passed to target when applying the call

A naïve implementation might look like target.apply(ctx, args), but below we’ll be using Reflect.apply(...arguments). We’ll dig deeper into the Reflect built-in later in the series. For now, just think of them as equivalent, and take into account that the value returned by the apply trap is also going to be used as the result of a function call through proxy.

var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments)
  }
}

Besides the obvious "being able to log all parameters of every function call for proxy", this trap can be used for parameter balancing and to tweak the results of a function call without changing the method itself – and without changing the calling code either.

The example below proxies a sum method through a twice trap handler that doubles the results of sum without affecting the code around it other than using the proxy instead of the sum method directly.

var twice = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments) * 2
  }
}
function sum (left, right) {
  return left + right
}
var proxy = new Proxy(sum, twice)
console.log(proxy(1, 2))
// <- 6
console.log(proxy(...[3, 4]))
// <- 14
console.log(proxy.call(null, 5, 6))
// <- 22
console.log(proxy.apply(null, [7, 8]))
// <- 30

Naturally, calling Reflect.apply on the proxy will be caught by the apply trap as well.

Reflect.apply(proxy, null, [9, 10])
// <- 38

What else would you use handler.apply for?

Tomorrow I’ll publish the last article on Proxy – Promise! – It’ll include the remaining trap handlers, such as construct and getPrototypeOf. Subscribe below so you don’t miss them.

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