Walter Breakell

Proxies

The Proxy object, introduced in ECMAScript 2015, allows us to intercept operations performed on objects. We can define custom behavior for these operations, allowing us to control exactly what occurs when one of these operations is performed. Let’s first look at how we might intercept calls to retrieve a property on an object.

let player = {
  active: true,
  rank: 2,
  score: 5000
};

let proxyPlayer = new Proxy(player, {
  get: function (target, property) {
    if (property === 'score') {
      return `${target[property]} points`;
    } else {
      return target[property];
    }
  }
});

In the example above, we are creating a new proxy object called proxyPlayer that will act as an intermediary between the original player object and the end user trying to access a property. The first parameter we provide when creating a new proxy object is the target object. This is the object that we want our proxy to wrap and intercept operations to. The second parameter we pass is a handler object with methods that define how we want to handle certain operations on this object. There are quite a few methods that this handler object supports, including property lookup, assignment, deletion, and enumeration. For a full list of supported handler methods, take a look at the proxy handler documentation on MDN.

In our example, we are passing an object literal with a get property that will specify the behavior when an object property is trying to be retrieved. In this case, we would like to return a player’s score with the string ‘points’ appended to the end. We can achieve this by first checking if we are accessing the score property and then returning a string that combines the value of that score property with the word ‘points’. It’s important to remember that we still need to return the normal value for all other object properties that we don’t want to affect. To account for these other object properties, we return the original value of all other object properties within the else block of the if statement.

Now if we retrieve the score property from the proxyPlayer object, we should see our new return value. No other object property lookups should be affected.

proxyPlayer.active // true
proxyPlayer.rank // 2
proxyPlayer.score // "5000 points"

Notice that we must access these object properties through the proxy and not through the original object itself. If I were to bypass the proxy, operating directly on the player object, I wouldn’t see this custom behavior.

Proxies also allow us to intercept function invocations. To demonstrate this, let’s add a method to our player object.

let player = {
  active: true,
  rank: 2,
  score: 5000,
  promote: function () {
    return ++this.rank;
  }
};

Here, we’ve added a promote method that increments the player’s rank and returns the new rank. Let’s say we only want to allow the player object to invoke this method, preventing any other objects from calling the promote function. To do this, we can create a proxy around the promote function, intercepting any calls to the function.

player.promote = new Proxy(player.promote, {
  apply: function (target, context, args) {
    if (context !== player) {
      return 'Only the player object can use promote';
    } else {
      return target.apply(context, args);
    }
  }
});

In this example, player.promote is now a proxy based on the original player.promote function. The original player.promote function is no longer accessible. Every call to the function now runs through this new proxy.

Within our new proxy, we are using the apply handler method to intercept any calls to this function. The apply handler method accepts three parameters: the target object, the context of the function invocation, and an array of arguments to the function. In our case, the target object is our promote function itself. The context is the object represented by the this keyword when the function is invoked. For example, if I call player.promote(), the context would be the player object.

Within the apply method we are verifying that the context for the function invocation is the player object before executing the function. If any other object tries to invoke the promote function, it will fail and return a string explaining that only the player object can use the promote function.

let other = {};
other.boost = player.promote;
other.boost(); // "Only the player object can use promote"

Proxies provide developers with a powerful way of manipulating the behavior and functionality of certain operations. Whether you need to implement some sort of validation logic, or just want to extend the behavior of some default operation, proxies can be a valuable tool.