Proxies
One of the most obviously meta programming features added to ES6 is the Proxy
feature.
A proxy is a special kind of object you create that “wraps” — or sits in front of — another normal object. You can register special handlers (aka traps) on the proxy object which are called when various operations are performed against the proxy. These handlers have the opportunity to perform extra logic in addition to forwarding the operations on to the original target/wrapped object.
One example of the kind of trap handler you can define on a proxy is get
that intercepts the [[Get]]
operation — performed when you try to access a property on an object. Consider:
var obj = { a: 1 },
handlers = {
get(target,key,context) {
// note: target === obj,
// context === pobj
console.log( "accessing: ", key );
return Reflect.get(
target, key, context
);
}
},
pobj = new Proxy( obj, handlers );
obj.a;
// 1
pobj.a;
// accessing: a
// 1
We declare a get(..)
handler as a named method on the handler object (second argument to Proxy(..)
), which receives a reference to the target object (obj
), the key property name ("a"
), and the self
/receiver/proxy (pobj
).
After the console.log(..)
tracing statement, we “forward” the operation onto obj
via Reflect.get(..)
. We will cover the Reflect
API in the next section, but note that each available proxy trap has a corresponding Reflect
function of the same name.
These mappings are symmetric on purpose. The proxy handlers each intercept when a respective meta programming task is performed, and the Reflect
utilities each perform the respective meta programming task on an object. Each proxy handler has a default definition that automatically calls the corresponding Reflect
utility. You will almost certainly use both Proxy
and Reflect
in tandem.
Here’s a list of handlers you can define on a proxy for a target object/function, and how/when they are triggered:
get(..)
: via[[Get]]
, a property is accessed on the proxy (Reflect.get(..)
,.
property operator, or[ .. ]
property operator)set(..)
: via[[Set]]
, a property value is set on the proxy (Reflect.set(..)
, the=
assignment operator, or destructuring assignment if it targets an object property)deleteProperty(..)
: via[[Delete]]
, a property is deleted from the proxy (Reflect.deleteProperty(..)
ordelete
)apply(..)
(if target is a function): via[[Call]]
, the proxy is invoked as a normal function/method (Reflect.apply(..)
,call(..)
,apply(..)
, or the(..)
call operator)construct(..)
(if target is a constructor function): via[[Construct]]
, the proxy is invoked as a constructor function (Reflect.construct(..)
ornew
)getOwnPropertyDescriptor(..)
: via[[GetOwnProperty]]
, a property descriptor is retrieved from the proxy (Object.getOwnPropertyDescriptor(..)
orReflect.getOwnPropertyDescriptor(..)
)defineProperty(..)
: via[[DefineOwnProperty]]
, a property descriptor is set on the proxy (Object.defineProperty(..)
orReflect.defineProperty(..)
)getPrototypeOf(..)
: via[[GetPrototypeOf]]
, the[[Prototype]]
of the proxy is retrieved (Object.getPrototypeOf(..)
,Reflect.getPrototypeOf(..)
,__proto__
,Object#isPrototypeOf(..)
, orinstanceof
)setPrototypeOf(..)
: via[[SetPrototypeOf]]
, the[[Prototype]]
of the proxy is set (Object.setPrototypeOf(..)
,Reflect.setPrototypeOf(..)
, or__proto__
)preventExtensions(..)
: via[[PreventExtensions]]
, the proxy is made non-extensible (Object.preventExtensions(..)
orReflect.preventExtensions(..)
)isExtensible(..)
: via[[IsExtensible]]
, the extensibility of the proxy is probed (Object.isExtensible(..)
orReflect.isExtensible(..)
)ownKeys(..)
: via[[OwnPropertyKeys]]
, the set of owned properties and/or owned symbol properties of the proxy is retrieved (Object.keys(..)
,Object.getOwnPropertyNames(..)
,Object.getOwnSymbolProperties(..)
,Reflect.ownKeys(..)
, orJSON.stringify(..)
)enumerate(..)
: via[[Enumerate]]
, an iterator is requested for the proxy’s enumerable owned and “inherited” properties (Reflect.enumerate(..)
orfor..in
)has(..)
: via[[HasProperty]]
, the proxy is probed to see if it has an owned or “inherited” property (Reflect.has(..)
,Object#hasOwnProperty(..)
, or"prop" in obj
)
Tip: For more information about each of these meta programming tasks, see the “Reflect
API” section later in this chapter.
In addition to the notations in the preceding list about actions that will trigger the various traps, some traps are triggered indirectly by the default actions of another trap. For example:
var handlers = {
getOwnPropertyDescriptor(target,prop) {
console.log(
"getOwnPropertyDescriptor"
);
return Object.getOwnPropertyDescriptor(
target, prop
);
},
defineProperty(target,prop,desc){
console.log( "defineProperty" );
return Object.defineProperty(
target, prop, desc
);
}
},
proxy = new Proxy( {}, handlers );
proxy.a = 2;
// getOwnPropertyDescriptor
// defineProperty
The getOwnPropertyDescriptor(..)
and defineProperty(..)
handlers are triggered by the default set(..)
handler’s steps when setting a property value (whether newly adding or updating). If you also define your own set(..)
handler, you may or may not make the corresponding calls against context
(not target
!) which would trigger these proxy traps.
Proxy Limitations
These meta programming handlers trap a wide array of fundamental operations you can perform against an object. However, there are some operations which are not (yet, at least) available to intercept.
For example, none of these operations are trapped and forwarded from pobj
proxy to obj
target:
var obj = { a:1, b:2 },
handlers = { .. },
pobj = new Proxy( obj, handlers );
typeof obj;
String( obj );
obj + "";
obj == pobj;
obj === pobj
Perhaps in the future, more of these underlying fundamental operations in the language will be interceptable, giving us even more power to extend JavaScript from within itself.
Warning: There are certain invariants — behaviors which cannot be overridden — that apply to the use of proxy handlers. For example, the result from the isExtensible(..)
handler is always coerced to a boolean
. These invariants restrict some of your ability to customize behaviors with proxies, but they do so only to prevent you from creating strange and unusual (or inconsistent) behavior. The conditions for these invariants are complicated so we won’t fully go into them here, but this post (http://www.2ality.com/2014/12/es6-proxies.html#invariants) does a great job of covering them.
Revocable Proxies
A regular proxy always traps for the target object, and cannot be modified after creation — as long as a reference is kept to the proxy, proxying remains possible. However, there may be cases where you want to create a proxy that can be disabled when you want to stop allowing it to proxy. The solution is to create a revocable proxy:
var obj = { a: 1 },
handlers = {
get(target,key,context) {
// note: target === obj,
// context === pobj
console.log( "accessing: ", key );
return target[key];
}
},
{ proxy: pobj, revoke: prevoke } =
Proxy.revocable( obj, handlers );
pobj.a;
// accessing: a
// 1
// later:
prevoke();
pobj.a;
// TypeError
A revocable proxy is created with Proxy.revocable(..)
, which is a regular function, not a constructor like Proxy(..)
. Otherwise, it takes the same two arguments: target and handlers.
The return value of Proxy.revocable(..)
is not the proxy itself as with new Proxy(..)
. Instead, it’s an object with two properties: proxy and revoke — we used object destructuring (see “Destructuring” in Chapter 2) to assign these properties to pobj
and prevoke()
variables, respectively.
Once a revocable proxy is revoked, any attempts to access it (trigger any of its traps) will throw a TypeError
.
An example of using a revocable proxy might be handing out a proxy to another party in your application that manages data in your model, instead of giving them a reference to the real model object itself. If your model object changes or is replaced, you want to invalidate the proxy you handed out so the other party knows (via the errors!) to request an updated reference to the model.
Using Proxies
The meta programming benefits of these Proxy handlers should be obvious. We can almost fully intercept (and thus override) the behavior of objects, meaning we can extend object behavior beyond core JS in some very powerful ways. We’ll look at a few example patterns to explore the possibilities.
Proxy First, Proxy Last
As we mentioned earlier, you typically think of a proxy as “wrapping” the target object. In that sense, the proxy becomes the primary object that the code interfaces with, and the actual target object remains hidden/protected.
You might do this because you want to pass the object somewhere that can’t be fully “trusted,” and so you need to enforce special rules around its access rather than passing the object itself.
Consider:
var messages = [],
handlers = {
get(target,key) {
// string value?
if (typeof target[key] == "string") {
// filter out punctuation
return target[key]
.replace( /[^\w]/g, "" );
}
// pass everything else through
return target[key];
},
set(target,key,val) {
// only set unique strings, lowercased
if (typeof val == "string") {
val = val.toLowerCase();
if (target.indexOf( val ) == -1) {
target.push(val);
}
}
return true;
}
},
messages_proxy =
new Proxy( messages, handlers );
// elsewhere:
messages_proxy.push(
"heLLo...", 42, "wOrlD!!", "WoRld!!"
);
messages_proxy.forEach( function(val){
console.log(val);
} );
// hello world
messages.forEach( function(val){
console.log(val);
} );
// hello... world!!
I call this proxy first design, as we interact first (primarily, entirely) with the proxy.
We enforce some special rules on interacting with messages_proxy
that aren’t enforced for messages
itself. We only add elements if the value is a string and is also unique; we also lowercase the value. When retrieving values from messages_proxy
, we filter out any punctuation in the strings.
Alternatively, we can completely invert this pattern, where the target interacts with the proxy instead of the proxy interacting with the target. Thus, code really only interacts with the main object. The easiest way to accomplish this fallback is to have the proxy object in the [[Prototype]]
chain of the main object.
Consider:
var handlers = {
get(target,key,context) {
return function() {
context.speak(key + "!");
};
}
},
catchall = new Proxy( {}, handlers ),
greeter = {
speak(who = "someone") {
console.log( "hello", who );
}
};
// setup `greeter` to fall back to `catchall`
Object.setPrototypeOf( greeter, catchall );
greeter.speak(); // hello someone
greeter.speak( "world" ); // hello world
greeter.everyone(); // hello everyone!
We interact directly with greeter
instead of catchall
. When we call speak(..)
, it’s found on greeter
and used directly. But when we try to access a method like everyone()
, that function doesn’t exist on greeter
.
The default object property behavior is to check up the [[Prototype]]
chain (see the this & Object Prototypes title of this series), so catchall
is consulted for an everyone
property. The proxy get()
handler then kicks in and returns a function that calls speak(..)
with the name of the property being accessed ("everyone"
).
I call this pattern proxy last, as the proxy is used only as a last resort.
“No Such Property/Method”
A common complaint about JS is that objects aren’t by default very defensive in the situation where you try to access or set a property that doesn’t already exist. You may wish to predefine all the properties/methods for an object, and have an error thrown if a nonexistent property name is subsequently used.
We can accomplish this with a proxy, either in proxy first or proxy last design. Let’s consider both.
var obj = {
a: 1,
foo() {
console.log( "a:", this.a );
}
},
handlers = {
get(target,key,context) {
if (Reflect.has( target, key )) {
return Reflect.get(
target, key, context
);
}
else {
throw "No such property/method!";
}
},
set(target,key,val,context) {
if (Reflect.has( target, key )) {
return Reflect.set(
target, key, val, context
);
}
else {
throw "No such property/method!";
}
}
},
pobj = new Proxy( obj, handlers );
pobj.a = 3;
pobj.foo(); // a: 3
pobj.b = 4; // Error: No such property/method!
pobj.bar(); // Error: No such property/method!
For both get(..)
and set(..)
, we only forward the operation if the target object’s property already exists; error thrown otherwise. The proxy object (pobj
) is the main object code should interact with, as it intercepts these actions to provide the protections.
Now, let’s consider inverting with proxy last design:
var handlers = {
get() {
throw "No such property/method!";
},
set() {
throw "No such property/method!";
}
},
pobj = new Proxy( {}, handlers ),
obj = {
a: 1,
foo() {
console.log( "a:", this.a );
}
};
// setup `obj` to fall back to `pobj`
Object.setPrototypeOf( obj, pobj );
obj.a = 3;
obj.foo(); // a: 3
obj.b = 4; // Error: No such property/method!
obj.bar(); // Error: No such property/method!
The proxy last design here is a fair bit simpler with respect to how the handlers are defined. Instead of needing to intercept the [[Get]]
and [[Set]]
operations and only forward them if the target property exists, we instead rely on the fact that if either [[Get]]
or [[Set]]
get to our pobj
fallback, the action has already traversed the whole [[Prototype]]
chain and not found a matching property. We are free at that point to unconditionally throw the error. Cool, huh?
Proxy Hacking the [[Prototype]]
Chain
The [[Get]]
operation is the primary channel by which the [[Prototype]]
mechanism is invoked. When a property is not found on the immediate object, [[Get]]
automatically hands off the operation to the [[Prototype]]
object.
That means you can use the get(..)
trap of a proxy to emulate or extend the notion of this [[Prototype]]
mechanism.
The first hack we’ll consider is creating two objects which are circularly linked via [[Prototype]]
(or, at least it appears that way!). You cannot actually create a real circular [[Prototype]]
chain, as the engine will throw an error. But a proxy can fake it!
Consider:
var handlers = {
get(target,key,context) {
if (Reflect.has( target, key )) {
return Reflect.get(
target, key, context
);
}
// fake circular `[[Prototype]]`
else {
return Reflect.get(
target[
Symbol.for( "[[Prototype]]" )
],
key,
context
);
}
}
},
obj1 = new Proxy(
{
name: "obj-1",
foo() {
console.log( "foo:", this.name );
}
},
handlers
),
obj2 = Object.assign(
Object.create( obj1 ),
{
name: "obj-2",
bar() {
console.log( "bar:", this.name );
this.foo();
}
}
);
// fake circular `[[Prototype]]` link
obj1[ Symbol.for( "[[Prototype]]" ) ] = obj2;
obj1.bar();
// bar: obj-1 <-- through proxy faking [[Prototype]]
// foo: obj-1 <-- `this` context still preserved
obj2.foo();
// foo: obj-2 <-- through [[Prototype]]
Note: We didn’t need to proxy/forward [[Set]]
in this example, so we kept things simpler. To be fully [[Prototype]]
emulation compliant, you’d want to implement a set(..)
handler that searches the [[Prototype]]
chain for a matching property and respects its descriptor behavior (e.g., set, writable). See the this & Object Prototypes title of this series.
In the previous snippet, obj2
is [[Prototype]]
linked to obj1
by virtue of the Object.create(..)
statement. But to create the reverse (circular) linkage, we create property on obj1
at the symbol location Symbol.for("[[Prototype]]")
(see “Symbols” in Chapter 2). This symbol may look sort of special/magical, but it isn’t. It just allows me a conveniently named hook that semantically appears related to the task I’m performing.
Then, the proxy’s get(..)
handler looks first to see if a requested key
is on the proxy. If not, the operation is manually handed off to the object reference stored in the Symbol.for("[[Prototype]]")
location of target
.
One important advantage of this pattern is that the definitions of obj1
and obj2
are mostly not intruded by the setting up of this circular relationship between them. Although the previous snippet has all the steps intertwined for brevity’s sake, if you look closely, the proxy handler logic is entirely generic (doesn’t know about obj1
or obj2
specifically). So, that logic could be pulled out into a simple helper that wires them up, like a setCircularPrototypeOf(..)
for example. We’ll leave that as an exercise for the reader.
Now that we’ve seen how we can use get(..)
to emulate a [[Prototype]]
link, let’s push the hackery a bit further. Instead of a circular [[Prototype]]
, what about multiple [[Prototype]]
linkages (aka “multiple inheritance”)? This turns out to be fairly straightforward:
var obj1 = {
name: "obj-1",
foo() {
console.log( "obj1.foo:", this.name );
},
},
obj2 = {
name: "obj-2",
foo() {
console.log( "obj2.foo:", this.name );
},
bar() {
console.log( "obj2.bar:", this.name );
}
},
handlers = {
get(target,key,context) {
if (Reflect.has( target, key )) {
return Reflect.get(
target, key, context
);
}
// fake multiple `[[Prototype]]`
else {
for (var P of target[
Symbol.for( "[[Prototype]]" )
]) {
if (Reflect.has( P, key )) {
return Reflect.get(
P, key, context
);
}
}
}
}
},
obj3 = new Proxy(
{
name: "obj-3",
baz() {
this.foo();
this.bar();
}
},
handlers
);
// fake multiple `[[Prototype]]` links
obj3[ Symbol.for( "[[Prototype]]" ) ] = [
obj1, obj2
];
obj3.baz();
// obj1.foo: obj-3
// obj2.bar: obj-3
Note: As mentioned in the note after the earlier circular [[Prototype]]
example, we didn’t implement the set(..)
handler, but it would be necessary for a complete solution that emulates [[Set]]
actions as normal [[Prototype]]
s behave.
obj3
is set up to multiple-delegate to both obj1
and obj2
. In obj3.baz()
, the this.foo()
call ends up pulling foo()
from obj1
(first-come, first-served, even though there’s also a foo()
on obj2
). If we reordered the linkage as obj2, obj1
, the obj2.foo()
would have been found and used.
But as is, the this.bar()
call doesn’t find a bar()
on obj1
, so it falls over to check obj2
, where it finds a match.
obj1
and obj2
represent two parallel [[Prototype]]
chains of obj3
. obj1
and/or obj2
could themselves have normal [[Prototype]]
delegation to other objects, or either could themself be a proxy (like obj3
is) that can multiple-delegate.
Just as with the circular [[Prototype]]
example earlier, the definitions of obj1
, obj2
, and obj3
are almost entirely separate from the generic proxy logic that handles the multiple-delegation. It would be trivial to define a utility like setPrototypesOf(..)
(notice the “s”!) that takes a main object and a list of objects to fake the multiple [[Prototype]]
linkage to. Again, we’ll leave that as an exercise for the reader.
Hopefully the power of proxies is now becoming clearer after these various examples. There are many other powerful meta programming tasks that proxies enable.