Using a Proxy as a Prototype
Proxies can be used as prototypes, but doing so is a bit more involved than the previous examples in this chapter. When a proxy is a prototype, the proxy traps are only called when the default operation would normally continue on to the prototype, which does limit a proxy’s capabilities as a prototype. Consider this example:
let target = {};
let newTarget = Object.create(new Proxy(target, {
// never called
defineProperty(trapTarget, name, descriptor) {
// would cause an error if called
return false;
}
}));
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
The newTarget
object is created with a proxy as the prototype. Making target
the proxy target effectively makes target
the prototype of newTarget
because the proxy is transparent. Now, proxy traps will only be called if an operation on newTarget
would pass the operation through to happen on target
.
The Object.defineProperty()
method is called on newTarget
to create an own property called name
. Defining a property on an object isn’t an operation that normally continues to the object’s prototype, so the defineProperty
trap on the proxy is never called and the name
property is added to newTarget
as an own property.
While proxies are severely limited when used as prototypes, there are a few traps that are still useful.
Using the get
Trap on a Prototype
When the internal [[Get]]
method is called to read a property, the operation looks for own properties first. If an own property with the given name isn’t found, then the operation continues to the prototype and looks for a property there. The process continues until there are no further prototypes to check.
Thanks to that process, if you set up a get
proxy trap, the trap will be called on a prototype whenever an own property of the given name doesn’t exist. You can use the get
trap to prevent unexpected behavior when accessing properties that you can’t guarantee will exist. Just create an object that throws an error whenever you try to access a property that doesn’t exist:
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// throw an error
let unknown = thing.unknown;
In this code, the thing
object is created with a proxy as its prototype. The get
trap throws an error when called to indicate that the given key doesn’t exist on the thing
object. When thing.name
is read, the operation never calls the get
trap on the prototype because the property exists on thing
. The get
trap is called only when the thing.unknown
property, which doesn’t exist, is accessed.
When the last line executes, unknown
isn’t an own property of thing
, so the operation continues to the prototype. The get
trap then throws an error. This type of behavior can be very useful in JavaScript, where unknown properties silently return undefined
instead of throwing an error (as happens in other languages).
It’s important to understand that in this example, trapTarget
and receiver
are different objects. When a proxy is used as a prototype, the trapTarget
is the prototype object itself while the receiver
is the instance object. In this case, that means trapTarget
is equal to target
and receiver
is equal to thing
. That allows you access both to the original target of the proxy and the object on which the operation is meant to take place.
Using the set
Trap on a Prototype
The internal [[Set]]
method also checks for own properties and then continues to the prototype if needed. When you assign a value to an object property, the value is assigned to the own property with the same name if it exists. If no own property with the given name exists, then the operation continues to the prototype. The tricky part is that even though the assignment operation continues to the prototype, assigning a value to that property will create a property on the instance (not the prototype) by default, regardless of whether a property of that name exists on the prototype.
To get a better idea of when the set
trap will be called on a prototype and when it won’t, consider the following example showing the default behavior:
let target = {};
let thing = Object.create(new Proxy(target, {
set(trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// triggers the `set` proxy trap
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// does not trigger the `set` proxy trap
thing.name = "boo";
console.log(thing.name); // "boo"
In this example, target
starts with no own properties. The thing
object has a proxy as its prototype that defines a set
trap to catch the creation of any new properties. When thing.name
is assigned "thing"
as its value, the set
proxy trap is called because thing
doesn’t have an own property called name
. Inside the set
trap, trapTarget
is equal to target
and receiver
is equal to thing
. The operation should ultimately create a new property on thing
, and fortunately Reflect.set()
implements this default behavior for you if you pass in receiver
as the fourth argument.
Once the name
property is created on thing
, setting thing.name
to a different value will no longer call the set
proxy trap. At that point, name
is an own property so the [[Set]]
operation never continues on to the prototype.
Using the has
Trap on a Prototype
Recall that the has
trap intercepts the use of the in
operator on objects. The in
operator searches first for an object’s own property with the given name. If an own property with that name doesn’t exist, the operation continues to the prototype. If there’s no own property on the prototype, then the search continues through the prototype chain until the own property is found or there are no more prototypes to search.
The has
trap is therefore only called when the search reaches the proxy object in the prototype chain. When using a proxy as a prototype, that only happens when there’s no own property of the given name. For example:
let target = {};
let thing = Object.create(new Proxy(target, {
has(trapTarget, key) {
return Reflect.has(trapTarget, key);
}
}));
// triggers the `has` proxy trap
console.log("name" in thing); // false
thing.name = "thing";
// does not trigger the `has` proxy trap
console.log("name" in thing); // true
This code creates a has
proxy trap on the prototype of thing
. The has
trap isn’t passed a receiver
object like the get
and set
traps are because searching the prototype happens automatically when the in
operator is used. Instead, the has
trap must operate only on trapTarget
, which is equal to target
. The first time the in
operator is used in this example, the has
trap is called because the property name
doesn’t exist as an own property of thing
. When thing.name
is given a value and then the in
operator is used again, the has
trap isn’t called because the operation stops after finding the own property name
on thing
.
The prototype examples to this point have centered around objects created using the Object.create()
method. But if you want to create a class that has a proxy as a prototype, the process is a bit more involved.
Proxies as Prototypes on Classes
Classes cannot be directly modified to use a proxy as a prototype because their prototype
property is non-writable. You can, however, use a bit of misdirection to create a class that has a proxy as its prototype by using inheritance. To start, you need to create an ECMAScript 5-style type definition using a constructor function. You can then overwrite the prototype to be a proxy. Here’s an example:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
let thing = new NoSuchProperty();
// throws error due to `get` proxy trap
let result = thing.name;
The NoSuchProperty
function represents the base from which the class will inherit. There are no restrictions on the prototype
property of functions, so you can overwrite it with a proxy. The get
trap is used to throw an error when the property doesn’t exist. The thing
object is created as an instance of NoSuchProperty
and throws an error when the nonexistent name
property is accessed.
The next step is to create a class that inherits from NoSuchProperty
. You can simply use the extends
syntax discussed in Chapter 9 to introduce the proxy into the class’ prototype chain, like this:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
// throws an error because "wdth" doesn't exist
let area2 = shape.length * shape.wdth;
The Square
class inherits from NoSuchProperty
so the proxy is in the Square
class’ prototype chain. The shape
object is then created as a new instance of Square
and has two own properties: length
and width
. Reading the values of those properties succeeds because the get
proxy trap is never called. Only when a property that doesn’t exist on shape
is accessed (shape.wdth
, an obvious typo) does the get
proxy trap trigger and throw an error.
That proves the proxy is in the prototype chain of shape
, but it might not be obvious that the proxy is not the direct prototype of shape
. In fact, the proxy is a couple of steps up the prototype chain from shape
. You can see this more clearly by slightly altering the preceding example:
function NoSuchProperty() {
// empty
}
// store a reference to the proxy that will be the prototype
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false
let secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true
This version of the code stores the proxy in a variable called proxy
so it’s easy to identify later. The prototype of shape
is Square.prototype
, which is not a proxy. But the prototype of Square.prototype
is the proxy that was inherited from NoSuchProperty
.
The inheritance adds another step in the prototype chain, and that matters because operations that might result in calling the get
trap on proxy
need to go through one extra step before getting there. If there’s a property on Square.prototype
, then that will prevent the get
proxy trap from being called, as in this example:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
let area2 = shape.getArea();
console.log(area2); // 12
// throws an error because "wdth" doesn't exist
let area3 = shape.length * shape.wdth;
Here, the Square
class has a getArea()
method. The getArea()
method is automatically added to Square.prototype
so when shape.getArea()
is called, the search for the method getArea()
starts on the shape
instance and then proceeds to its prototype. Because getArea()
is found on the prototype, the search stops and the proxy is never called. That is actually the behavior you want in this situation, as you wouldn’t want to incorrectly throw an error when getArea()
was called.
Even though it takes a little bit of extra code to create a class with a proxy in its prototype chain, it can be worth the effort if you need such functionality.