What is a JavaScript proxy? you might ask. It is one of the features that shipped with ES6. Sadly, it seems not to be widely used.

According to the MDN Web Docs:

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

In simple terms, proxies are getters and setters with lots of swag. A proxy object sits between an object and the outside world. They intercept calls to the attributes and methods of an object even if those attributes and methods don’t exist.

For us to understand how proxies work, we need to define three terms used by proxies:

  1. handler: The placeholder object which contains traps (they’re the interceptors).
  2. traps: The methods that provide property access (they live inside the handler).
  3. target: The object which the proxy virtualizes.

Syntax

let myProxy = new Proxy(target, handler);

Why proxies?

Since proxies are similar to getters and setters, why should we use them? Let’s see why:

const staff = {
  _name: "Jane Doe",
  _age: 25,
  get name() {
    console.log(this._name);
  },
  get age() {
    console.log(this._age);
  },
  set age(newAge) {
    this._age = newAge;
    console.log(this._age)
  }
};
staff.name // => "Jane Doe"
staff.age // => 25
staff.age = 30
staff.age // => 30
staff.position // => undefined

Let’s write the same code with proxies:

const staff = {
  name: "Jane Doe",
  age: 25
}
const handler = {
  get: (target, name) => {
    name in target ? console.log(target[name]) : console.log('404 not found');
  },
  set: (target, name, value) => {
    target[name] = value;
  }
}
const staffProxy = new Proxy(staff, handler);
staffProxy.name // => "Jane Doe"
staffProxy.age // => 25
staffProxy.age = 30
staffProxy.age // => 30
staffProxy.position // => '404 not found'

In the above example using getters and setters, we have to define a getter and setter for each attribute in the staff object. When we try to access a non-existing property, we get undefined.

With proxies, we only need one get and set trap to manage interactions with every property in the staff object. Whenever we try to access a non-existing property, we get a custom error message.

There are many other use cases for proxies. Let’s explore some:

Validation with proxies

With proxies, we can enforce value validations in JavaScript objects. Let’s say we have a staff schema and would like to perform some validations before a staff can be saved:

const validator = {
  set: (target, key, value) => {
    const allowedProperties = ['name', 'age', 'position'];
    if (!allowedProperties.includes(key)) {
      throw new Error(`${key} is not a valid property`)
    }
    
    if (key === 'age') {
      if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) {
        throw new TypeError('Age must be a positive number')
      }
    }
    if (key === 'name' || key === 'position') {
      if (typeof value !== 'string' || value.length <= 0) {
        throw new TypeError(`${key} must be a valid string`)
      }
    }
   target[key] = value; // save the value
   return true; // indicate success
  }
}
const staff = new Proxy({}, validator);
staff.stats = "malicious code" //=> Uncaught Error: stats is not a valid property
staff.age = 0 //=> Uncaught TypeError: Age must be a positive number
staff.age = 10
staff.age //=> 10
staff.name = '' //=> Uncaught TypeError: name must be a valid string

In the code snippet above, we declare a validator handler where we have an array of allowedProperties. In the set trap, we check if the key being set is part of our allowedProperties. If it’s not, we throw an error. We also check if the values being set are of certain data types before we save the value.

Revocable proxies

What if we wanted to revoke access to an object? Well, JavaScript proxies have a Proxy.revocable() method which creates a revocable proxy. This gives us the ability to revoke access to a proxy. Let’s see how it works:

const handler = {
  get: (target, name) => {
    name in target ? console.log(target[name]) : console.log('404 not found');
    console.log(target)
  },
  
  set: (target, name, value) => {
    target[name] = value;
  }
}
const staff = {
  name: "Jane Doe",
  age: 25
}
let { proxy, revoke } = Proxy.revocable(staff, handler);
proxy.age // => 25
proxy.name // => "Jane Doe"
proxy.age = 30
proxy.age // => 30
revoke() // revoke access to the proxy
proxy.age // => Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.age = 30 // => Uncaught TypeError: Cannot perform 'set' on a proxy that has been revoked

In the example above, we are using destructuring to access theproxy and revoke properties of the object returned by Proxy.revocable().

After we call the revoke function, any operation applied to proxy causes a TypeError. With this in our code, we can prevent users from taking certain actions on certain objects.

JavaScript proxies are a powerful way to create and manage interactions between objects. Other real world applications for Proxies include:

  • Extending constructors
  • Manipulating DOM nodes
  • Value correction and an extra property
  • Tracing property accesses
  • Trapping function calls

And the list goes on.

There’s more to proxies than we have covered here. You can check the Proxy MDN Docs to find out all the available traps and how to use them.

I hope you found this tutorial useful. Please do and share so others can find this article. Hit me up on Twitter @developia_ with questions or for a chat.