5 min read

proxies

An Introduction to ES6 Proxies


The Vue reactivity system was given a complete overall in Vue 3 using ES6 Proxies. Although not as well known as other ES6 additions and enhancements, ES6 proxies 'virtualize' an object to control both the default and subsequently modified behavior of the object as required.

Similar to using a proxy server in a hosting environment, one can create a proxy object that wraps an existing object and intercepts low-level object operations (such as .set(), .get(), .deleteProperty()) to redefine their current behavior without modifying the original object.

Known as a form of meta-programming whereby code is developed to manipulate itself, the use of proxy objects are quite often used in the creation of software libraries (npm repository for example) intended to be consumed by other developers.

The goal is to ensure that exposed objects behave the way they were intended to behave and are used the way in which they were intended to be consumed.

For example, one can delete object properties delete obj.prop. But what if you want to restrict end-users of your software library from deleting a specific exposed object property that may not be necessary for their code needs, but in fact, is neccessary for the proper execution of that object within the software library?

In this case, you can create a proxy object that prevents specific object properties from being deleted and still allow other object properties to be safely deleted if by doing so does not create any unintended negative consequences as determined by the core developer.

This way the core developer of the software library still maintains required control of objects exposed to other developers thereby ensuring the software library objects are used properly and as intended.

Let's put this use case into practice. But before we begin, we need to learn some Proxy terminology:

  • Target: object to be 'virtualized' by the proxy by means of a wrapper

  • Handler: object placeholder that contains the traps

  • Traps: functions that manipulate the behavior of object properties

First we instantiate a new Proxy instance passing target and handler as arguments:

const proxy = new Proxy(target, handler);

Next we need the target to be wrapped by the Proxy instance:

let target = {
  firstName: 'John',
  middleName: 'David',
  lastName: 'Doe'
};

Lastly, we need the handler to perform the desired property operations on the target. In this example, the middleName property can be deleted but the firstName and lastName properties cannot be deleted:

const handler = {
  // deleteProperty trap
  deleteProperty(obj, key) {
    if (key === 'middleName') {
      return Reflect.deleteProperty(obj, key);
    } else {
      return false;
    }
  }
};
// code execution
delete proxy.firstName;
delete proxy.middleName;
delete proxy.lastName;
console.log(proxy);   // { firstName: 'John', lastName: 'Doe' }
console.log(target);  // { firstName: 'John', lastName: 'Doe' }

Notice that we are using the Reflect object with the deleteProperty() static method that was also introduced in ES6. Please refer to this comparison between the ES6 Reflect object and ES5 Object: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/Comparing_Reflect_and_Object_methods

Upon examination, both the Reflect.set() and Reflect.get() static methods are available that were not previously available with ES6 Object. So let's quickly have a look at both of those implementations.

Reflect.set() will set an object property and returns true if successful, false otherwise. Reflect.get() returns the property value if the property key exists, otherwise returns undefined.

We will extend our previous example to both set and get a object property to our target object defined above using a new handler object.

In this example, the object property nickname is not allowed to be set. When the property age is set, validate that the property type is not a string, otherwise return. And if an object property key does not exist when the get method is executed, the string 'NOT FOUND' should be returned instead of undefined as per normal.

const handler = {
  get(obj, key) {
    return obj[key] || 'NOT FOUND';
  },
  set(obj, key, value) {
    if (key === 'nickname') {
      return;
    }

    if (key === 'age' && typeof value === 'string') {
      return
    }

    obj[key] = value;
  }
};
// code execution
proxy.nickname = 'Fuzzy';
console.log(proxy);   // { firstName: 'John', middleName: 'David, lastName: 'Doe' }
console.log(target);  // { firstName: 'John', middleName: 'David, lastName: 'Doe' }
console.log(proxy.nickname);  // NOT FOUND

proxy.age = '25';
console.log(proxy.age);   // NOT FOUND

proxy.age = 25;
console.log(proxy);   // { firstName: 'John', middleName: 'David, lastName: 'Doe', age: 25 }
console.log(target);  // { firstName: 'John', middleName: 'David, lastName: 'Doe', age: 25 }

We can see with the last example that object property validation is an excellent use case for ES6 proxies. This post presents a basic introduction as to why one would use a proxy and how one would go about implementing a proxy.

Google to find any number of quality blog posts that delve into more advanced use cases such as explicitly controlling your REST API verbs and response behavior. But in the meantime, I suggest you start here at the MDN Proxy docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy.