Prototype Proxy Traps

Chapter 4 introduced the Object.setPrototypeOf() method that ECMAScript 6 added to complement the ECMAScript 5 Object.getPrototypeOf() method. Proxies allow you to intercept execution of both methods through the setPrototypeOf and getPrototypeOf traps. In both cases, the method on Object calls the trap of the corresponding name on the proxy, allowing you to alter the methods’ behavior.

Since there are two traps associated with prototype proxies, there’s a set of methods associated with each type of trap. The setPrototypeOf trap receives these arguments:

  1. trapTarget - the object for which the prototype should be set (the proxy’s target)
  2. proto - the object to use for as the prototype

These are the same arguments passed to the Object.setPrototypeOf() and Reflect.setPrototypeOf() methods. The getPrototypeOf trap, on the other hand, only receives the trapTarget argument, which is the argument passed to the Object.getPrototypeOf() and Reflect.getPrototypeOf() methods.

How Prototype Proxy Traps Work

There are some restrictions on these traps. First, the getPrototypeOf trap must return an object or null, and any other return value results in a runtime error. The return value check ensures that Object.getPrototypeOf() will always return an expected value. Similarly, the return value of the setPrototypeOf trap must be false if the operation doesn’t succeed. When setPrototypeOf returns false, Object.setPrototypeOf() throws an error. If setPrototypeOf returns any value other than false, then Object.setPrototypeOf() assumes the operation succeeded.

The following example hides the prototype of the proxy by always returning null and also doesn’t allow the prototype to be changed:

  1. let target = {};
  2. let proxy = new Proxy(target, {
  3. getPrototypeOf(trapTarget) {
  4. return null;
  5. },
  6. setPrototypeOf(trapTarget, proto) {
  7. return false;
  8. }
  9. });
  10. let targetProto = Object.getPrototypeOf(target);
  11. let proxyProto = Object.getPrototypeOf(proxy);
  12. console.log(targetProto === Object.prototype); // true
  13. console.log(proxyProto === Object.prototype); // false
  14. console.log(proxyProto); // null
  15. // succeeds
  16. Object.setPrototypeOf(target, {});
  17. // throws error
  18. Object.setPrototypeOf(proxy, {});

This code emphasizes the difference between the behavior of target and proxy. While Object.getPrototypeOf() returns a value for target, it returns null for proxy because the getPrototypeOf trap is called. Similarly, Object.setPrototypeOf() succeeds when used on target but throws an error when used on proxy due to the setPrototypeOf trap.

If you want to use the default behavior for these two traps, you can use the corresponding methods on Reflect. For instance, this code implements the default behavior for the getPrototypeOf and setPrototypeOf traps:

  1. let target = {};
  2. let proxy = new Proxy(target, {
  3. getPrototypeOf(trapTarget) {
  4. return Reflect.getPrototypeOf(trapTarget);
  5. },
  6. setPrototypeOf(trapTarget, proto) {
  7. return Reflect.setPrototypeOf(trapTarget, proto);
  8. }
  9. });
  10. let targetProto = Object.getPrototypeOf(target);
  11. let proxyProto = Object.getPrototypeOf(proxy);
  12. console.log(targetProto === Object.prototype); // true
  13. console.log(proxyProto === Object.prototype); // true
  14. // succeeds
  15. Object.setPrototypeOf(target, {});
  16. // also succeeds
  17. Object.setPrototypeOf(proxy, {});

In this example, you can use target and proxy interchangeably and get the same results because the getPrototypeOf and setPrototypeOf traps are just passing through to use the default implementation. It’s important that this example use the Reflect.getPrototypeOf() and Reflect.setPrototypeOf() methods rather than the methods of the same name on Object due to some important differences.

Why Two Sets of Methods?

The confusing aspect of Reflect.getPrototypeOf() and Reflect.setPrototypeOf() is that they look suspiciously similar to the Object.getPrototypeOf() and Object.setPrototypeOf() methods. While both sets of methods perform similar operations, there are some distinct differences between the two.

To begin, Object.getPrototypeOf() and Object.setPrototypeOf() are higher-level operations that were created for developer use from the start. The Reflect.getPrototypeOf() and Reflect.setPrototypeOf() methods are lower-level operations that give developers access to the previously internal-only [[GetPrototypeOf]] and [[SetPrototypeOf]] operations. The Reflect.getPrototypeOf() method is the wrapper for the internal [[GetPrototypeOf]] operation (with some input validation). The Reflect.setPrototypeOf() method and [[SetPrototypeOf]] have the same relationship. The corresponding methods on Object also call [[GetPrototypeOf]] and [[SetPrototypeOf]] but perform a few steps before the call and inspect the return value to determine how to behave.

The Reflect.getPrototypeOf() method throws an error if its argument is not an object, while Object.getPrototypeOf() first coerces the value into an object before performing the operation. If you were to pass a number into each method, you’d get a different result:

  1. let result1 = Object.getPrototypeOf(1);
  2. console.log(result1 === Number.prototype); // true
  3. // throws an error
  4. Reflect.getPrototypeOf(1);

The Object.getPrototypeOf() method allows you to retrieve a prototype for the number 1 because it first coerces the value into a Number object and then returns Number.prototype. The Reflect.getPrototypeOf() method doesn’t coerce the value, and since 1 isn’t an object, it throws an error.

The Reflect.setPrototypeOf() method also has a few more differences from the Object.setPrototypeOf() method. First, Reflect.setPrototypeOf() returns a boolean value indicating whether the operation was successful. A true value is returned for success, and false is returned for failure. If Object.setPrototypeOf() fails, it throws an error.

As the first example under “How Prototype Proxy Traps Work” showed, when the setPrototypeOf proxy trap returns false, it causes Object.setPrototypeOf() to throw an error. The Object.setPrototypeOf() method returns the first argument as its value and therefore isn’t suitable for implementing the default behavior of the setPrototypeOf proxy trap. The following code demonstrates these differences:

  1. let target1 = {};
  2. let result1 = Object.setPrototypeOf(target1, {});
  3. console.log(result1 === target1); // true
  4. let target2 = {};
  5. let result2 = Reflect.setPrototypeOf(target2, {});
  6. console.log(result2 === target2); // false
  7. console.log(result2); // true

In this example, Object.setPrototypeOf() returns target1 as its value, but Reflect.setPrototypeOf() returns true. This subtle difference is very important. You’ll see more seemingly duplicate methods on Object and Reflect, but always be sure to use the method on Reflect inside any proxy traps.

I> Both sets of methods will call the getPrototypeOf and setPrototypeOf proxy traps when used on a proxy.