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:
trapTarget
- the object for which the prototype should be set (the proxy’s target)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:
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// succeeds
Object.setPrototypeOf(target, {});
// throws error
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:
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// succeeds
Object.setPrototypeOf(target, {});
// also succeeds
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:
let result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype); // true
// throws an error
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:
let target1 = {};
let result1 = Object.setPrototypeOf(target1, {});
console.log(result1 === target1); // true
let target2 = {};
let result2 = Reflect.setPrototypeOf(target2, {});
console.log(result2 === target2); // false
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.