“I am delighted to support Nicolás’ endeavor because his book looks exactly like what people who are coming to JavaScript with fresh eyes need.”
– Brendan Eich
Ideal for professional software developers with a basic understanding of JavaScript, this practical book shows you how to build small, interconnected ES6 JavaScript modules that emphasize reusability. You’ll learn how to face a project with a modular mindset, and how to organize your applications into simple pieces that work well in isolation and can be combined to create a large, robust application.
This book focuses on two aspects of JavaScript development: modularity and ES6 features. You’ll learn how to tackle application development by following a scale-out approach. As pieces of your codebase grow too big, you can break them up into smaller modules.
The book can be read online for free or purchased through Amazon.
This book is part of the Modular JavaScript series.
Start with the book series launch announcement on Pony Foo
Participate in the crowdfunding campaign on Indiegogo
Amplify the announcement on social media via Thunderclap
Share a message on Twitter or within your social circles
Contribute to the source code repository on GitHub
Read the free HTML version of the book on Pony Foo
Purchase the book from O’Reilly on Amazon
Chapter 6
Managing Property Access with Proxies
Proxies are an interesting and powerful feature in ES6 that act as intermediaries between API consumers and objects. In a nutshell, you can use a Proxy
to determine the desired behavior whenever the properties of an underlying target
object are accessed. A handler
object can be used to configure traps for your Proxy
, which define and restrict how the underlying object is accessed, as we’ll see in a bit.
Getting Started with Proxy
By default, proxies don’t do much—in fact they don’t do anything. If you don’t provide any configuration, your proxy will just work as a pass-through to the target
object, also known as a “no-op forwarding proxy,” meaning that all operations on the proxy object defer to the underlying object.
In the following piece of code, we create a no-op forwarding Proxy
. You can observe how by assigning a value to proxy.exposed
, that value is passed onto target.exposed
. You could think of proxies as the gatekeepers of their underlying objects: they may allow certain operations to go through and prevent others from passing, but they carefully inspect every single interaction with their underlying objects.
const
target
=
{}
const
handler
=
{}
const
proxy
=
new
Proxy
(
target
,
handler
)
proxy
.
exposed
=
true
console
.
log
(
target
.
exposed
)
// <- true
console
.
log
(
proxy
.
somethingElse
)
// <- undefined
We can make the proxy object a bit more interesting by adding traps. Traps allow you to intercept interactions with target
in several different ways, as long as those interactions happen through the proxy
object. For instance, we could use a get
trap to log every attempt to pull a value out of a property in target
, or a set
trap to prevent certain properties from being written to. Let’s kick things off by learning more about get
traps.
Trapping get Accessors
The proxy
in the following code listing 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 returned by accessing any given property before returning a value to the accessor.
const
handler
=
{
get
(
target
,
key
)
{
console
.
log
(
`Get on property "
${
key
}
"`
)
return
target
[
key
]
}
}
const
target
=
{}
const
proxy
=
new
Proxy
(
target
,
handler
)
proxy
.
numbers
=
[
1
,
1
,
2
,
3
,
5
,
8
,
13
]
proxy
.
numbers
// 'Get on property "numbers"'
// <- [1, 1, 2, 3, 5, 8, 13]
proxy
[
'something-else'
]
// 'Get on property "something-else"'
// <- undefined
As a complement to proxies, ES6 introduces a Reflect
built-in object. The traps in ES6 proxies are mapped one-to-one to the Reflect
API: for every trap, there’s a matching reflection method in Reflect
. These methods can be particularly useful when we want the default behavior of proxy traps, but we don’t want to concern ourselves with the implementation of that behavior.
In the following code snippet we use Reflect.get
to provide the default behavior for get
operations, while not worrying about accessing the key
property in target
by hand. While in this case the operation may seem trivial, the default behavior for other traps may be harder to remember and implement correctly. We can forward every parameter in the trap to the reflection API and return its result.
const
handler
=
{
get
(
target
,
key
)
{
console
.
log
(
`Get on property "
${
key
}
"`
)
return
Reflect
.
get
(
target
,
key
)
}
}
const
target
=
{}
const
proxy
=
new
Proxy
(
target
,
handler
)
The get
trap doesn’t necessarily have to return the original target[key]
value. Imagine the case where you wanted properties prefixed by an underscore to be inaccessible. In this case, you could throw an error, letting the consumer know that the property is inaccessible through the proxy.
const
handler
=
{
get
(
target
,
key
)
{
if
(
key
.
startsWith
(
'_'
))
{
throw
new
Error
(
`Property "
${
key
}
" is inaccessible.`
)
}
return
Reflect
.
get
(
target
,
key
)
}
}
const
target
=
{}
const
proxy
=
new
Proxy
(
target
,
handler
)
proxy
.
_secret
// <- Uncaught Error: Property "_secret" is inaccessible.
To the keen observer, it may be apparent that disallowing access to certain properties through the proxy becomes most useful when creating a proxy with clearly defined access rules for the underlying target
object, and exposing that proxy instead of the target
object. That way you can still access the underlying object freely, but consumers are forced to go through the proxy and play by its rules, putting you in control of exactly how they can interact with the object. This wasn’t possible before proxies were introduced in in ES6.
Trapping set Accessors
As the in counterpart of get
traps, set
traps can intercept property assignment. Suppose we wanted to prevent assignment on properties starting with an underscore. We could replicate the get
trap we implemented earlier to block assignment as well.
The Proxy
in the next example prevents underscored property access for both get
and set
when accessing target
through proxy
. Note how the set
trap returns true
here? Returning true
in a set
trap means that setting the property key
to the provided 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. If we were using Reflect.set
instead, as brought up earlier, we wouldn’t need to concern ourselves with these implementation details: we could just return Reflect.set(target, key, value)
. That way, when somebody reads our code later, they’ll be able to understand that we’re using Reflect.set
, which is equivalent to the default operation, equivalent to the case where a Proxy
object isn’t part of the equation.
const
handler
=
{
get
(
target
,
key
)
{
invariant
(
key
,
'get'
)
return
Reflect
.
get
(
target
,
key
)
},
set
(
target
,
key
,
value
)
{
invariant
(
key
,
'set'
)
return
Reflect
.
set
(
target
,
key
,
value
)
}
}
function
invariant
(
key
,
action
)
{
if
(
key
.
startsWith
(
'_'
))
{
throw
new
Error
(
`Can't
${
action
}
private "
${
key
}
"
property`
)
}
}
const
target
=
{}
const
proxy
=
new
Proxy
(
target
,
handler
)
The following piece of code demonstrates how the proxy
responds to consumer interaction.
proxy
.
text
=
'the great black pony ate your lunch'
console
.
log
(
target
.
text
)
// <- 'the great black pony ate your lunch'
proxy
.
_secret
// <- Error: Can't get private "_secret" property
proxy
.
_secret
=
'invalidate'
// <- Error: Can't set private "_secret" property
The object being proxied, target
in our latest example, should be completely hidden from consumers, so that they are forced to access it exclusively through proxy
. Preventing direct access to the target
object means that they will have to obey the access rules defined on the proxy
object—such as “properties prefixed with an underscore are off-limits.”
To that end, you could wrap the proxied object in a function and then return the proxy
.
function
proxied
()
{
const
target
=
{}
const
handler
=
{
get
(
target
,
key
)
{
invariant
(
key
,
'get'
)
return
Reflect
.
get
(
target
,
key
)
},
set
(
target
,
key
,
value
)
{
invariant
(
key
,
'set'
)
return
Reflect
.
set
(
target
,
key
,
value
)
}
}
return
new
Proxy
(
target
,
handler
)
}
function
invariant
(
key
,
action
)
{
if
(
key
.
startsWith
(
'_'
))
{
throw
new
Error
(
`Can't
${
action
}
private "
${
key
}
"
property`
)
}
}
Usage stays the same, except that now access to target
is completely governed by proxy
and its mischievous traps. At this point, any _secret
properties in target
are completely inaccessible through the proxy, and since target
can’t be accessed directly from outside the proxied
function, they’re sealed off from consumers for good.
A general-purpose approach would be to offer a proxying function that takes an original
object and returns a proxy. You can then call that function whenever you’re about to expose a public API, as shown in the following code block. The concealWithPrefix
function wraps the original
object in a Proxy
where properties prefixed with a prefix
value (or _
if none is provided) can’t be accessed.
function
concealWithPrefix
(
original
,
prefix
=
'_'
)
{
const
handler
=
{
get
(
original
,
key
)
{
invariant
(
key
,
'get'
)
return
Reflect
.
get
(
original
,
key
)
},
set
(
original
,
key
,
value
)
{
invariant
(
key
,
'set'
)
return
Reflect
.
set
(
original
,
key
,
value
)
}
}
return
new
Proxy
(
original
,
handler
)
function
invariant
(
key
,
action
)
{
if
(
key
.
startsWith
(
prefix
))
{
throw
new
Error
(
`Can't
${
action
}
private "
${
key
}
"
property`
)
}
}
}
const
target
=
{
_secret
:
'secret'
,
text
:
'everyone-can-read-this'
}
const
proxy
=
concealWithPrefix
(
target
)
// expose proxy to consumers
You might be tempted to argue that you could achieve the same behavior in ES5 simply by using variables privately scoped to the concealWithPrefix
function, without the need for the Proxy
itself. The difference is that proxies allow you to “privatize” property access dynamically. Without relying on Proxy
, you couldn’t mark every property that starts with an underscore as private. You could use Object.freeze
1 on the object, but then you wouldn’t be able to modify the property references yourself, either. Or you could define get and set accessors for every property, but then again you wouldn’t be able to block access on every single property, only the ones you explicitly configured getters and in setters for.
Object.freeze
method prevents adding new properties, removing existing ones, and modifying property value references. Note that it doesn’t make the values themselves immutable: their properties can still change, provided Object.freeze
isn’t called on those objects as well.