Proxies in ECMAScript

What are they?

Proxies give us greater control over what happens when we interact with objects. They wrap a target object and expose some of the internal operations on it. This means we can customise what happens when we get or set a property, create a new instance of an object, call a function, and more (see the full list of the low-level operations we can intercept here).

Some layman definitions:

  • Proxy: A wrapper object that exposes internal operations of a target object
  • Target: The object the proxy is wrapping
  • Handler: The proxy looks in this object for any trap methods you may have provided
  • Trap: A function added to a handler object that tells the proxy what to do when a specific method on the target is called

Example

Here's an example of a proxy that adds logging for getting and setting a property on a target object.

  // this is our target object
  let friend = {
    name: "Luna Lovegood",
    house: "Ravenclaw",
    patronus: "Hare"
  };

  // the handler for the proxy
  let handler = {
    // trap method
    // this method will be called when we read a property on the proxy object
    get(trapTarget, key, receiver) {
      console.log(`Getting property "${key}"`);
      // The Reflect object provides us with the default implementation
      // There's a Reflect method for all proxy trap methods
      return Reflect.get(trapTarget, key, receiver);
    },
    // trap method
    // this method will be called when we set a property on the proxy object
    set(trapTarget, key, value, receiver) {
      console.log(`Setting property "${key}"`);
      return Reflect.set(trapTarget, key, value, receiver);
    }
  };

  // creating the proxy– we pass in the friend (target) and the handler
  let friendProxy = new Proxy(friend, handler);

  friendProxy.name;
  // Getting property "name"
  // 'Luna Lovegood'

  friendProxy.hobby = "Painting";
  // Setting property "hobby"
  // 'Painting'

We used the get() and set() trap methods there to intercept calls that get and set properties on the target object through the proxy. Since the proxy virtualises the target object, any operation available on the target object can be called from the proxy.

Using proxies for validation

Proxies can be used to add validation to objects. I'll cover two use cases here– checking the arguments passed into a function match some validation criteria, and checking for typos in property names.

Parameter validation

Since there is an apply() trap method available to us, we can add validation to make sure that the arguments we pass into a function are what we would expect.

Let's create a speak() function with the following requirements:

  • We don't want to say more than one thing at a time
  • We only want to speak in strings
  // this is our target object
  function speak(message) {
    console.log(`Raising your head, you declare to the room "${message}"`);
  }

  let speakProxy = new Proxy(speak, {
    // trap method
    apply(trapTarget, thisArg, argumentsList) {
      if (argumentsList.length !== 1) {
        throw new Error("Please say one thing at a time");
      }
      if (typeof argumentsList[0] !== "string") {
        throw new TypeError("Please use your words");
      }
      // call the apply method on the Reflect object to get the default
      // behaviour
      return Reflect.apply(trapTarget, thisArg, argumentsList);
    }
  });

  speakProxy("Wingardium Leviosa");
  // Raising your head, you declare to the room "Wingardium Leviosa"
  speakProxy("Wingardium Leviosa", "Accio Firebolt!");
  // Error: Please say one thing at a time
  speakProxy(42); // TypeError: Please use your words

Here we have defined a speak() method that prints out a given message. Firstly, we added the apply() trap method to the handler object. This method is executed when we call any function on the proxy object.

Next, we added two conditions– the first checks that there is exactly 1 argument passed into the function, the other checks that the type of the argument is a string. If we're happy that the argument satisfies our validation criteria, then we call Reflect.apply() to execute the default implementation.

Checking for typos in property names

When a property cannot be found on an object, the default behaviour is to return undefined. This behaviour can sometimes lead to hard to track down bugs, especially in larger in projects. We can guard against this using proxies.

  let friend = {
    name: "Luna Lovegood",
    house: "Ravenclaw",
    patronus: "Hare"
  };

  friend.house; // Ravenclaw
  friend.hoose; // undefined

  let friendProxy = new Proxy(friend, {
    get(trapTarget, key, receiver) {
      if (!(key in receiver))
        throw new TypeError("Property not recognised");
      return Reflect.get(trapTarget, key, receiver);
    }
  });

  friend.house; // Ravenclaw
  friend.hoose; // undefined (the validation exists on the proxy, not the target)

  friendProxy.house; // Ravenclaw
  friendProxy.hoose; // TypeError: Property not recognised.

We can add similar validation to make sure we are only ever setting a property that already exists. This isn't always what we want, but it's still an interesting example:

  let friend = {
    name: "Andy Stabler",
    house: "Hufflepuff",
    patronus: undefined
  };

  friend.patronus = "Salmon";
  friend.patroonus = "Stag";
  friend;
  // { name: 'Andy Stabler',
  //  house: 'Hufflepuff',
  //  patronus: 'Salmon',
  //  patroonus: 'Stag' }
  // Whoopsy! We accidentally set a patroonus property!

  let friendProxy = new Proxy(friend, {
    get(trapTarget, key, receiver) {
      if (!(key in receiver))
        throw new TypeError("Property not recognised.");
      return Reflect.get(trapTarget, key, receiver);
    },
    set(trapTarget, key, value, receiver) {
      if (!(key in receiver))
        throw new TypeError("Property not recognised.");
      return Reflect.set(trapTarget, key, value, receiver);
    }
  });

  delete friend.patroonus;
  friendProxy.patronus = "Stag";
  friendProxy.patroonus = "Wolf"; // TypeError: Property not recognised.

In both the get() and set() trap methods we performed a check to make sure that the key was in the receiver object. The in operator looks in the object and its prototype chain for a property with a matching key. If the property is not found, then a TypeError is raised.

Where can I learn more?

I've only scratched the surface of proxies and their many uses in ECMAScript. You can find out a lot more using the following resources:

Previous post: Typed Arrays in ECMAScript

Next post: Offline Development