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:

  1. let target = {};
  2. let newTarget = Object.create(new Proxy(target, {
  3. // never called
  4. defineProperty(trapTarget, name, descriptor) {
  5. // would cause an error if called
  6. return false;
  7. }
  8. }));
  9. Object.defineProperty(newTarget, "name", {
  10. value: "newTarget"
  11. });
  12. console.log(newTarget.name); // "newTarget"
  13. 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:

  1. let target = {};
  2. let thing = Object.create(new Proxy(target, {
  3. get(trapTarget, key, receiver) {
  4. throw new ReferenceError(`${key} doesn't exist`);
  5. }
  6. }));
  7. thing.name = "thing";
  8. console.log(thing.name); // "thing"
  9. // throw an error
  10. 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:

  1. let target = {};
  2. let thing = Object.create(new Proxy(target, {
  3. set(trapTarget, key, value, receiver) {
  4. return Reflect.set(trapTarget, key, value, receiver);
  5. }
  6. }));
  7. console.log(thing.hasOwnProperty("name")); // false
  8. // triggers the `set` proxy trap
  9. thing.name = "thing";
  10. console.log(thing.name); // "thing"
  11. console.log(thing.hasOwnProperty("name")); // true
  12. // does not trigger the `set` proxy trap
  13. thing.name = "boo";
  14. 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:

  1. let target = {};
  2. let thing = Object.create(new Proxy(target, {
  3. has(trapTarget, key) {
  4. return Reflect.has(trapTarget, key);
  5. }
  6. }));
  7. // triggers the `has` proxy trap
  8. console.log("name" in thing); // false
  9. thing.name = "thing";
  10. // does not trigger the `has` proxy trap
  11. 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:

  1. function NoSuchProperty() {
  2. // empty
  3. }
  4. NoSuchProperty.prototype = new Proxy({}, {
  5. get(trapTarget, key, receiver) {
  6. throw new ReferenceError(`${key} doesn't exist`);
  7. }
  8. });
  9. let thing = new NoSuchProperty();
  10. // throws error due to `get` proxy trap
  11. 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:

  1. function NoSuchProperty() {
  2. // empty
  3. }
  4. NoSuchProperty.prototype = new Proxy({}, {
  5. get(trapTarget, key, receiver) {
  6. throw new ReferenceError(`${key} doesn't exist`);
  7. }
  8. });
  9. class Square extends NoSuchProperty {
  10. constructor(length, width) {
  11. super();
  12. this.length = length;
  13. this.width = width;
  14. }
  15. }
  16. let shape = new Square(2, 6);
  17. let area1 = shape.length * shape.width;
  18. console.log(area1); // 12
  19. // throws an error because "wdth" doesn't exist
  20. 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:

  1. function NoSuchProperty() {
  2. // empty
  3. }
  4. // store a reference to the proxy that will be the prototype
  5. let proxy = new Proxy({}, {
  6. get(trapTarget, key, receiver) {
  7. throw new ReferenceError(`${key} doesn't exist`);
  8. }
  9. });
  10. NoSuchProperty.prototype = proxy;
  11. class Square extends NoSuchProperty {
  12. constructor(length, width) {
  13. super();
  14. this.length = length;
  15. this.width = width;
  16. }
  17. }
  18. let shape = new Square(2, 6);
  19. let shapeProto = Object.getPrototypeOf(shape);
  20. console.log(shapeProto === proxy); // false
  21. let secondLevelProto = Object.getPrototypeOf(shapeProto);
  22. 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:

  1. function NoSuchProperty() {
  2. // empty
  3. }
  4. NoSuchProperty.prototype = new Proxy({}, {
  5. get(trapTarget, key, receiver) {
  6. throw new ReferenceError(`${key} doesn't exist`);
  7. }
  8. });
  9. class Square extends NoSuchProperty {
  10. constructor(length, width) {
  11. super();
  12. this.length = length;
  13. this.width = width;
  14. }
  15. getArea() {
  16. return this.length * this.width;
  17. }
  18. }
  19. let shape = new Square(2, 6);
  20. let area1 = shape.length * shape.width;
  21. console.log(area1); // 12
  22. let area2 = shape.getArea();
  23. console.log(area2); // 12
  24. // throws an error because "wdth" doesn't exist
  25. 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.