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:

  1. var obj = { a: 1 },
  2. handlers = {
  3. get(target,key,context) {
  4. // note: target === obj,
  5. // context === pobj
  6. console.log( "accessing: ", key );
  7. return Reflect.get(
  8. target, key, context
  9. );
  10. }
  11. },
  12. pobj = new Proxy( obj, handlers );
  13. obj.a;
  14. // 1
  15. pobj.a;
  16. // accessing: a
  17. // 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(..) or delete)
  • 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(..) or new)
  • getOwnPropertyDescriptor(..): via [[GetOwnProperty]], a property descriptor is retrieved from the proxy (Object.getOwnPropertyDescriptor(..) or Reflect.getOwnPropertyDescriptor(..))
  • defineProperty(..): via [[DefineOwnProperty]], a property descriptor is set on the proxy (Object.defineProperty(..) or Reflect.defineProperty(..))
  • getPrototypeOf(..): via [[GetPrototypeOf]], the [[Prototype]] of the proxy is retrieved (Object.getPrototypeOf(..), Reflect.getPrototypeOf(..), __proto__, Object#isPrototypeOf(..), or instanceof)
  • 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(..) or Reflect.preventExtensions(..))
  • isExtensible(..): via [[IsExtensible]], the extensibility of the proxy is probed (Object.isExtensible(..) or Reflect.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(..), or JSON.stringify(..))
  • enumerate(..): via [[Enumerate]], an iterator is requested for the proxy’s enumerable owned and “inherited” properties (Reflect.enumerate(..) or for..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:

  1. var handlers = {
  2. getOwnPropertyDescriptor(target,prop) {
  3. console.log(
  4. "getOwnPropertyDescriptor"
  5. );
  6. return Object.getOwnPropertyDescriptor(
  7. target, prop
  8. );
  9. },
  10. defineProperty(target,prop,desc){
  11. console.log( "defineProperty" );
  12. return Object.defineProperty(
  13. target, prop, desc
  14. );
  15. }
  16. },
  17. proxy = new Proxy( {}, handlers );
  18. proxy.a = 2;
  19. // getOwnPropertyDescriptor
  20. // 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:

  1. var obj = { a:1, b:2 },
  2. handlers = { .. },
  3. pobj = new Proxy( obj, handlers );
  4. typeof obj;
  5. String( obj );
  6. obj + "";
  7. obj == pobj;
  8. 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:

  1. var obj = { a: 1 },
  2. handlers = {
  3. get(target,key,context) {
  4. // note: target === obj,
  5. // context === pobj
  6. console.log( "accessing: ", key );
  7. return target[key];
  8. }
  9. },
  10. { proxy: pobj, revoke: prevoke } =
  11. Proxy.revocable( obj, handlers );
  12. pobj.a;
  13. // accessing: a
  14. // 1
  15. // later:
  16. prevoke();
  17. pobj.a;
  18. // 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:

  1. var messages = [],
  2. handlers = {
  3. get(target,key) {
  4. // string value?
  5. if (typeof target[key] == "string") {
  6. // filter out punctuation
  7. return target[key]
  8. .replace( /[^\w]/g, "" );
  9. }
  10. // pass everything else through
  11. return target[key];
  12. },
  13. set(target,key,val) {
  14. // only set unique strings, lowercased
  15. if (typeof val == "string") {
  16. val = val.toLowerCase();
  17. if (target.indexOf( val ) == -1) {
  18. target.push(val);
  19. }
  20. }
  21. return true;
  22. }
  23. },
  24. messages_proxy =
  25. new Proxy( messages, handlers );
  26. // elsewhere:
  27. messages_proxy.push(
  28. "heLLo...", 42, "wOrlD!!", "WoRld!!"
  29. );
  30. messages_proxy.forEach( function(val){
  31. console.log(val);
  32. } );
  33. // hello world
  34. messages.forEach( function(val){
  35. console.log(val);
  36. } );
  37. // 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:

  1. var handlers = {
  2. get(target,key,context) {
  3. return function() {
  4. context.speak(key + "!");
  5. };
  6. }
  7. },
  8. catchall = new Proxy( {}, handlers ),
  9. greeter = {
  10. speak(who = "someone") {
  11. console.log( "hello", who );
  12. }
  13. };
  14. // setup `greeter` to fall back to `catchall`
  15. Object.setPrototypeOf( greeter, catchall );
  16. greeter.speak(); // hello someone
  17. greeter.speak( "world" ); // hello world
  18. 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.

  1. var obj = {
  2. a: 1,
  3. foo() {
  4. console.log( "a:", this.a );
  5. }
  6. },
  7. handlers = {
  8. get(target,key,context) {
  9. if (Reflect.has( target, key )) {
  10. return Reflect.get(
  11. target, key, context
  12. );
  13. }
  14. else {
  15. throw "No such property/method!";
  16. }
  17. },
  18. set(target,key,val,context) {
  19. if (Reflect.has( target, key )) {
  20. return Reflect.set(
  21. target, key, val, context
  22. );
  23. }
  24. else {
  25. throw "No such property/method!";
  26. }
  27. }
  28. },
  29. pobj = new Proxy( obj, handlers );
  30. pobj.a = 3;
  31. pobj.foo(); // a: 3
  32. pobj.b = 4; // Error: No such property/method!
  33. 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:

  1. var handlers = {
  2. get() {
  3. throw "No such property/method!";
  4. },
  5. set() {
  6. throw "No such property/method!";
  7. }
  8. },
  9. pobj = new Proxy( {}, handlers ),
  10. obj = {
  11. a: 1,
  12. foo() {
  13. console.log( "a:", this.a );
  14. }
  15. };
  16. // setup `obj` to fall back to `pobj`
  17. Object.setPrototypeOf( obj, pobj );
  18. obj.a = 3;
  19. obj.foo(); // a: 3
  20. obj.b = 4; // Error: No such property/method!
  21. 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:

  1. var handlers = {
  2. get(target,key,context) {
  3. if (Reflect.has( target, key )) {
  4. return Reflect.get(
  5. target, key, context
  6. );
  7. }
  8. // fake circular `[[Prototype]]`
  9. else {
  10. return Reflect.get(
  11. target[
  12. Symbol.for( "[[Prototype]]" )
  13. ],
  14. key,
  15. context
  16. );
  17. }
  18. }
  19. },
  20. obj1 = new Proxy(
  21. {
  22. name: "obj-1",
  23. foo() {
  24. console.log( "foo:", this.name );
  25. }
  26. },
  27. handlers
  28. ),
  29. obj2 = Object.assign(
  30. Object.create( obj1 ),
  31. {
  32. name: "obj-2",
  33. bar() {
  34. console.log( "bar:", this.name );
  35. this.foo();
  36. }
  37. }
  38. );
  39. // fake circular `[[Prototype]]` link
  40. obj1[ Symbol.for( "[[Prototype]]" ) ] = obj2;
  41. obj1.bar();
  42. // bar: obj-1 <-- through proxy faking [[Prototype]]
  43. // foo: obj-1 <-- `this` context still preserved
  44. obj2.foo();
  45. // 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:

  1. var obj1 = {
  2. name: "obj-1",
  3. foo() {
  4. console.log( "obj1.foo:", this.name );
  5. },
  6. },
  7. obj2 = {
  8. name: "obj-2",
  9. foo() {
  10. console.log( "obj2.foo:", this.name );
  11. },
  12. bar() {
  13. console.log( "obj2.bar:", this.name );
  14. }
  15. },
  16. handlers = {
  17. get(target,key,context) {
  18. if (Reflect.has( target, key )) {
  19. return Reflect.get(
  20. target, key, context
  21. );
  22. }
  23. // fake multiple `[[Prototype]]`
  24. else {
  25. for (var P of target[
  26. Symbol.for( "[[Prototype]]" )
  27. ]) {
  28. if (Reflect.has( P, key )) {
  29. return Reflect.get(
  30. P, key, context
  31. );
  32. }
  33. }
  34. }
  35. }
  36. },
  37. obj3 = new Proxy(
  38. {
  39. name: "obj-3",
  40. baz() {
  41. this.foo();
  42. this.bar();
  43. }
  44. },
  45. handlers
  46. );
  47. // fake multiple `[[Prototype]]` links
  48. obj3[ Symbol.for( "[[Prototype]]" ) ] = [
  49. obj1, obj2
  50. ];
  51. obj3.baz();
  52. // obj1.foo: obj-3
  53. // 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.