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 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.
has
– trapsin
operatordeleteProperty
– trapsdelete
operatordefineProperty
– trapsObject.defineProperty
and declarative alternativesenumerate
– trapsfor..in
loopsownKeys
– trapsObject.keys
and related methodsapply
– traps function calls
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 tokey in target
- We can
return false
(ortrue
) – even thoughkey
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.
- When we wanted to ensure cross-browser support of getters and setters
- 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 undeletableenumerable: false
hides the property fromfor..in
loops andObject.keys
value: undefined
is the initial value for the propertywritable: false
makes the property value immutableget: undefined
is a method that acts as the getter for the propertyset: undefined
is a method that receives the newvalue
and updates the property’svalue
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 propertiesObject.getOwnPropertySymbols()
– just symbol propertiesObject.keys()
– just non-symbol enumerable propertiesReflect.ownKeys()
– we’ll get toReflect
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 proxiedctx
– the context passed asthis
totarget
when applying a callargs
– the arguments passed totarget
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.
Comments