Using a JavaScript Proxy Object to Control Object Operations
Tags: javascript, oop, webdev, tutorial
Published: 2020-01-05 22:12:43

In JavaScript, a Proxy is an object that lets us control what happens when we do some operation. For example, we can use them to control the lookup, assignment, enumeration of properties or how functions are invoked.

The Proxy constructor takes 2 arguments. The first is the target, which is the object that you want to apply the controlling operations to and the second is the handler which is an object that actually controls how operations behave in the target object, also called traps.

The handler object is the object that contains traps for the Proxy. It has a number of methods to let us control the fundamental operations of the object. The object has a number of methods to trap various operations that are done by the methods in the Object constructor. They include:

*   `handler.getPrototypeOf()` — lets us control the behavior of the `Object.getPrototypeOf()` method for the target object
*   `handler.setPrototypeOf()`— lets us control the behavior of the `Object.setPrototypeOf()` method for the target object
*   `handler.isExtensible()`— lets us control the behavior of the `Object.isExtensible()` method for the target object
*   `handler.preventExtensions()`— lets us control the behavior of the `Object.preventExtensions()` method for the target object
*   `handler.getOwnPropertyDescriptor()`— lets us control the behavior of the `Object.getOwnPropertyDescriptor()` method for the target object
*   `handler.defineProperty()`— lets us control the behavior of the `Object.defineProperty()` method for the target object
*   `handler.has()`— lets us control the behavior of the `Object.has()` method for the target object
*   `handler.get()`— lets us control the behavior of the `Object.get()` method for the target object
*   `handler.set()`— lets us control the behavior of the `Object.set()` method for the target object
*   `handler.deleteProperty()`— lets us control the behavior of the `Object.deleteProperty()` method for the target object
*   `handler.ownKeys()`— lets us control the behavior of the `Object.ownKeys()` method for the target object
*   `handler.apply()`— lets us control the behavior of the `Object.apply()` method for the target object
*   `handler.construct()`— lets us control the behavior of the `Object.construct()` method for the target object

A basic example would be to return a default value for a property with the Proxy. For example, if we have the following code:

```javascript
const handler = {  
  get(obj, prop) {  
    return prop === 'a' && obj[prop] ?  
      obj[prop] :  
      1;  
  }  
};
let p = new Proxy({}, handler);  
console.log(p.a); // 1  
p.a = 2;  
console.log(p.a); // 2
```

Then the first `console.log` statement would output 1 and the second one would output 2. This is because in the `handler` object, we have a `get` function to modify how a property is retrieved. In the function, if the property name is `a` and `obj[prop]` is truthy, which means that `obj['a']` is truthy, then we return it, otherwise, we return 1. This sets the default value of `p.a` where `p` is the Proxy object constructed by the Proxy constructor to 1. If we set a new value for `p.a` then the `get` function will return the new value since it’s truthy. Therefore, the second `console.log` statement of `p.a` outputs 2.

We can pass in an empty object for the `handler` argument. It would make all the default operations to the target object be forwarded as-is. For example, if we have:

```
let p = new Proxy({}, {});  
console.log(p.a); // undefined  
p.a = 2;  
console.log(p.a); // 2
```

Then we get that the first `console.log` statement is `undefined`, but the second one is 2 because we didn’t modify the `get` function in the handler object to return anything if nothing is set.

Also, we can use proxies for validation of values that are assigned to an object’s properties. For example, we can use it to validate that a valid US phone number is assigned to a property of the Proxy object:

```
const handler = {  
  set(obj, prop, value) {  
    const validPhone = /^\d{3}-\d{3}-\d{4}$/.test(value);  
    if (prop === 'phoneNumber') {  
      if (!validPhone) {  
        throw new Error('Invalid phone number');  
      }  
    } 
    obj[prop] = value;  
    return validPhone;  
  }  
};

let person = new Proxy({}, handler);
person.phoneNumber = '555-555-5555'; // valid  
console.log(person.phoneNumber);  
person.phoneNumber = 'abc'; // throws an error
```

In the example above, we check that what’s being assigned is actually a valid US phone number by checking against the given regular expression. If the `phoneNumber` property is being assigned, then we check the regular expression against the `value` given in the parameters of the `set` function, and if the `validPhone` is `false`, then we throw an error. Otherwise, we set the value to the `phoneNumber` property of the object. In the end, we return the validation status of the given `value`. The first assignment:

```javascript
person.phoneNumber = '555-555-5555';
```

This should work since it matches the regular expression in the `set` function. However, the second assignment would throw an error because it doesn’t match the regular expression given.

We can add the `construct` function to the `handler` object to extend the constructor of the `target` object. For example, we can extend a base object with the superclass by setting the base object’s prototype to the superclass and then create a new proxy with a `handler` object that has the `construct` and `apply` functions to control the behavior of the constructor and the `apply` functions of the base object. For example, we can write:

```javascript
function extend(sup, base) {  
  const descriptor = Object.getOwnPropertyDescriptor(  
    base.prototype, 'constructor'  
  );  
  base.prototype = Object.create(sup.prototype);  
  const handler = {  
    construct(target, args) {  
      const obj = Object.create(base.prototype);  
      this.apply(target, obj, args);  
      return obj;  
    },  
    apply(target, that, args) {  
      sup.apply(that, args);  
      base.apply(that, args);  
    }  
  };  
  const proxy = new Proxy(base, handler);  
  descriptor.value = proxy;  
  Object.defineProperty(base.prototype, 'constructor', descriptor);  
  return proxy;  
}

let Person = function(name) {  
  this.name = name;  
};

let Boy = extend(Person, function(name, age, gender) {  
  this.name = name;  
  this.age = age;  
  this.gender = gender;  
});

let Joe = new Boy('Joe', 13, 'M');  
console.log(Joe.gender);  
console.log(Joe.name);  
console.log(Joe.age);
```

This creates a proxy with the `base` object as a target. The `handler` has the `constructor` and the `apply` functions to modify the behavior of the constructor for the `base` object and the `apply` function respectively.

The `construct` function creates a new object `obj` by setting the prototype to the `sup` object which served as the superclass of the `obj` object, which in JavaScript is the same as the prototype. This is a template object which the `base` object inherits its members from. Then `this.apply(target, obj, args);` to run the constructor with the passed in arguments in the `args` object and then return the `obj` object. The `apply` function in the `handler` runs the constructor functions for both the `sup` and `base` objects to construct the base object.

Then at the end, the Proxy object is created and set as the constructor’s value with the `descriptor.value = proxy;` line. Then we set the `base` object’s prototype’s constructor by running `Object.defineProperty(base.prototype, ‘constructor’, descriptor);` and return the Proxy object to let us extend the constructor of the base object with a superclass object.

Below the `extend` function, we created a `Person` constructor to let us set the `name` property of instances of `Person`. Then we call the `extend` function with the `Person` object and pass in a new constructor function to with parameters for the `name`, `age`, and `gender` to set these properties. Then we get a new constructed object with:

```javascript
let Joe = new Boy('Joe', 13, 'M');
```

Then when we log the properties we get ‘M’ for `gender`, ‘Joe’ for `name`, and 13 for `age`.


When we set one property of a Proxy object, we can simultaneously modify another property of the object. For example, if we have a `room` Proxy object which is constructed with a `target` object with the `people` property that has an array of names of people in the same room, and we want to push to the `people` array when the `lastPerson` property of the Proxy object is set. We do can this with the following code:

```javascript
let room = new Proxy({  
  people: ['Joe', 'Jane']  
}, {  
  get(obj, prop) {  
    if (prop === 'lastPerson') {  
      return obj.people\[obj.people.length - 1\];  
    }  
    return obj[prop];  
  },  
  set(obj, prop, value) {  
    if (prop === 'lastPerson') {  
      obj.people.push(value);  
      obj[prop] = value;  
      return true;  
    }  
    return true;  
  }  
});

console.log(room);  
room.lastPerson = 'John';  
console.log(room.people);  
console.log(room.lastPerson);room.lastPerson = 'Mary';  
console.log(room.people);  
console.log(room.lastPerson);
```

In the `get` function, we specify that the value of the `lastPerson` of the property will be the last element of the `people` array. Therefore, when we run `console.log` on `room.lastPerson`, we always get the last element of the `room.people` array. Otherwise, we set the object as is. In the `set` function, when the `lastPerson` property is being modified then we also push whatever value is being set into the `people` array in the `room` Proxy object. Therefore, when we run the `console.log` statements, we get:

```javascript
["Joe", "Jane", "John"]  
John

["Joe", "Jane", "John", "Mary"]  
Mary
```

As we can see, when we set the `lastPerson` property of `room` we also get the same value pushed into the `people` array.

Below is a more comprehensive example of the traps we can set in the `handler` object to control the behavior of the proxy object’s operations:

```
const handler = {  
  get(obj, prop) {  
    return obj[prop];  
  },  
  set(obj, prop, value) {  
    obj[prop] = value;  
    return true;  
  },  
  deleteProperty(obj, prop) {  
    delete obj[prop];  
    return false;  
  },  
  ownKeys(obj) {  
    return Reflect.ownKeys(obj);  
  },  
  has(obj, prop) {  
    return prop in obj;  
  },  
  defineProperty(obj, prop, descriptor) {  
    Object.defineProperty(obj, prop, descriptor)  
    return true;  
  },  
  getOwnPropertyDescriptor(obj, prop) {  
    return Object.getOwnPropertyDescriptor(obj, prop);  
  },  
}

let proxy = new Proxy({}, handler);  
proxy.a = 1;  
console.log(proxy.a);  
console.log(Object.getOwnPropertyDescriptor(proxy, 'a'))  
console.log(Object.defineProperty(proxy, 'b', {  
  value: 1  
}))  
console.log('a' in proxy);  
console.log(delete proxy.c);  
console.log(Object.keys(proxy));
```

In the example above, the `getOwnProperty` function in the `handler` object controls the behavior of the `Object.getOwnPropertyDescriptor()` when it’s applied to the `proxy` object. The `defineProperty` function in the `handler` object controls how the `Object.defineProperty()` behaves when it’s called on the `proxy` object. The `has` function controls the behavior of the `in` operator, and the `deleteProperty` function controls the value that the `delete` operator returns when running with the `proxy` object as the operand. We returned `false` instead of the usual `true` when we use the `delete` operator on the `proxy`. The `ownKeys` function modifies the behavior of the `Object.keys()` method by enumerating the keys of an object with the `Reflect` object.

Wrapping up
-----------

JavaScript Proxies are a useful way to control the behavior of object operations. We can control how the object operators like the `in`, `delete`, and assignment operator behaves on the target object and its properties. This is very useful for validation during those operations and also handy for modifying the return values of operations that can return values like the `in` and `delete` operator.

We can modify the assignment operator with the `set` function where we can validate the value being assigned and also modify other properties at the same time. We can modify the behavior of the `get` function to return different values for properties in certain situations like when no value is set on a property.