ES6 in Action: How to Use Proxies

Share this article

ES6 in Action: Proxies

In computing terms, proxies sit between you and the things you’re communicating with. The term is most often applied to a proxy server — a device between the web browser (Chrome, Firefox, Safari, Edge etc.) and the web server (Apache, Nginx, IIS etc.) where a page is located. The proxy server can modify requests and responses. For example, it can increase efficiency by caching regularly accessed assets and serving them to multiple users.

ES6 proxies sit between your code and an object. A proxy allows you to perform meta-programming operations such as intercepting a call to inspect or change an object’s property.

The following terminology is used in relation to ES6 proxies:

target The original object the proxy will virtualize. This could be a JavaScript object such as the jQuery library or native objects such as arrays or even another proxies.

handler An object which implements the proxy’s behavior using…

traps Functions defined in the handler which provide access to the target when specific properties or methods are called.

It’s best explained with a simple example. We’ll create a target object named target which has three properties:

const target = {
  a: 1,
  b: 2,
  c: 3
};

We’ll now create a handler object which intercepts all get operations. This returns the target’s property when it’s available or 42 otherwise:

const handler = {
  get: function(target, name) {
    return (
      name in target ? target[name] : 42
    );
  }
};

We now create a new Proxy by passing the target and handler objects. Our code can interact with the proxy rather than accessing the target object directly:

const proxy = new Proxy(target, handler);

console.log(proxy.a);  // 1
console.log(proxy.b);  // 2
console.log(proxy.c);  // 3
console.log(proxy.meaningOfLife);  // 42

Let’s expand the proxy handler further so it only permits single-character properties from a to z to be set:

const handler = {
  get: function(target, name) {
    return (name in target ? target[name] : 42);
  },

  set: function(target, prop, value) {
    if (prop.length == 1 && prop >= 'a' && prop <= 'z') {
      target[prop] = value;
      return true;
    }
    else {
      throw new ReferenceError(prop + ' cannot be set');
      return false;
    }
  }
};

const proxy = new Proxy(target, handler);

proxy.a = 10;
proxy.b = 20;
proxy.ABC = 30;
// Exception: ReferenceError: ABC cannot be set

Proxy Trap Types

We’ve seen the get and set in action which are likely to be the most useful traps. However, there are several other trap types you can use to supplement proxy handler code:

  • construct(target, argList) Traps the creation of a new object with the new operator.
  • get(target, property) Traps Object.get() and must return the property’s value.
  • set(target, property, value) Traps Object.set() and must set the property value. Return true if successful. In strict mode, returning false will throw a TypeError exception.
  • deleteProperty(target, property) Traps a delete operation on an object’s property. Must return either true or false.
  • apply(target, thisArg, argList) Traps object function calls.
  • has(target, property) Traps in operators and must return either true or false.
  • ownKeys(target) Traps Object.getOwnPropertyNames() and must return an enumerable object.
  • getPrototypeOf(target) Traps Object.getPrototypeOf() and must return the prototype’s object or null.
  • setPrototypeOf(target, prototype) Traps Object.setPrototypeOf() to set the prototype object. No value is returned.
  • isExtensible(target) Traps Object.isExtensible(), which determines whether an object can have new properties added. Must return either true or false.
  • preventExtensions(target) Traps Object.preventExtensions(), which prevents new properties from being added to an object. Must return either true or false.
  • getOwnPropertyDescriptor(target, property) Traps Object.getOwnPropertyDescriptor(), which returns undefined or a property descriptor object with attributes for value, writable, get, set, configurable and enumerable.
  • defineProperty(target, property, descriptor) Traps Object.defineProperty() which defines or modifies an object property. Must return true if the target property was successfully defined or false if not.

Proxy Example 1: Profiling

Proxies allow you to create generic wrappers for any object without having to change the code within the target objects themselves.

In this example, we’ll create a profiling proxy which counts the number of times a property is accessed. First, we require a makeProfiler factory function which returns the Proxy object and retains the count state:

// create a profiling Proxy
function makeProfiler(target) {

  const
    count = {},
    handler = {

      get: function(target, name) {
        if (name in target) {
          count[name] = (count[name] || 0) + 1;
          return target[name];
        }
      }
    };

  return {
    proxy: new Proxy(target, handler),
    count: count
  }
};

We can now apply this proxy wrapper to any object or another proxy. For example:

const myObject = {
  h: 'Hello',
  w: 'World'
};

// create a myObject proxy
const pObj = makeProfiler(myObject);

// access properties
console.log(pObj.proxy.h); // Hello
console.log(pObj.proxy.h); // Hello
console.log(pObj.proxy.w); // World
console.log(pObj.count.h); // 2
console.log(pObj.count.w); // 1

While this is a trivial example, imagine the effort involved if you had to perform property access counts in several different objects without using proxies.

Proxy Example 2: Two-way Data Binding

Data binding synchronizes objects. It’s typically used in JavaScript MVC libraries to update an internal object when the DOM changes and vice versa.

Presume we have an input field with an ID of inputname:

<input type="text" id="inputname" value="" />

We also have a JavaScript object named myUser with an id property which references this input:

// internal state for #inputname field
const myUser = {
  id: 'inputname',
  name: ''
};

Our first objective is to update myUser.name when a user changes the input value. This can be achieved with an onchange event handler on the field:

inputChange(myUser);

// bind input to object
function inputChange(myObject) {
  if (!myObject || !myObject.id) return;

  const input = document.getElementById(myObject.id);
  input.addEventListener('onchange', function(e) {
    myObject.name = input.value;
  });
}

Our next objective is to update the input field when we modify myUser.name within JavaScript code. This is not as simple, but proxies offer a solution:

// proxy handler
const inputHandler = {
  set: function(target, prop, newValue) {
    if (prop == 'name' && target.id) {
      // update object property
      target[prop] = newValue;

      // update input field value
      document.getElementById(target.id).value = newValue;
      return true;
    }
    else return false;
  }
}

// create proxy
const myUserProxy = new Proxy(myUser, inputHandler);

// set a new name
myUserProxy.name = 'Craig';
console.log(myUserProxy.name); // Craig
console.log(document.getElementById('inputname').value); // Craig

This may not be the most efficient data-binding option, but proxies allow you to alter the behavior of many existing objects without changing their code.

Further Examples

Hemanth.HM’s article Negative Array Index in JavaScript suggests using proxies to implement negative array indexes. For example, arr[-1] returns the last element, arr[-2] returns the second-to-last element, and so on.

Nicholas C. Zakas’ article Creating type-safe properties with ECMAScript 6 proxies illustrates how proxies can be used to implement type safety by validating new values. In the example above, we could verify myUserProxy.name was always set to a string and throw an error otherwise.

Proxy Support

The power of proxies may not be immediately obvious, but they offer powerful meta-programming opportunities. Brendan Eich, the creator of JavaScript, thinks Proxies are Awesome!

Currently, proxy support is implemented in Node and all current browsers, with the exception of Internet Explorer 11. However, please note that not all browsers support all traps. You can get a better idea of what’s supported by consulting this browser compatibility table on the MDN Proxy page.

Unfortunately, it’s not possible to polyfill or transpile ES6 proxy code using tools such as Babel, because proxies are powerful and have no ES5 equivalent.

Frequently Asked Questions (FAQs) about ES6 Proxies

What are the practical use cases of ES6 Proxies in JavaScript?

ES6 Proxies are powerful tools in JavaScript that allow you to intercept and customize operations performed on objects. They are commonly used in various scenarios such as data binding, tracing property accesses, revocable references, and more. For instance, in data binding, proxies can be used to track changes to an object and update the DOM in real time. In tracing property accesses, proxies can log all attempts to access properties of an object, which can be useful for debugging. Revocable references allow you to control access to an object, which can be revoked at any time.

How do ES6 Proxies differ from traditional JavaScript objects?

Traditional JavaScript objects allow direct operations such as reading and writing properties. However, ES6 Proxies provide a layer of abstraction between the object and the outside world. This means you can control and customize how operations are performed on the object. For example, you can intercept attempts to read or write properties, delete properties, and even apply a function.

Can you explain the concept of ‘traps’ in ES6 Proxies?

In the context of ES6 Proxies, ‘traps’ are methods that provide property access. They ‘intercept’ the interaction with the target object. When an operation is performed on the proxy object, the corresponding trap is triggered, allowing you to customize how the operation is handled. For example, the ‘get’ trap is triggered when a property is read, and the ‘set’ trap is triggered when a property is written.

How can I create a revocable Proxy in ES6?

A revocable Proxy can be created using the Proxy.revocable() method. This method returns an object from which the proxy can be accessed and a revoke function that can be used to disable all operations on the proxy. Once the revoke function is called, any operation on the proxy will throw an exception.

Can ES6 Proxies be used with arrays?

Yes, ES6 Proxies can be used with arrays. The proxy can intercept and customize operations performed on the array, such as reading and writing elements, changing the length of the array, and more. This can be useful for validating array operations, logging changes, and more.

Are there any performance implications when using ES6 Proxies?

While ES6 Proxies are powerful, they can have performance implications. Since a proxy intercepts all operations performed on an object, it can slow down execution if not used properly. Therefore, it’s important to use proxies judiciously and only when necessary.

Can I use ES6 Proxies to intercept function calls?

Yes, ES6 Proxies can be used to intercept function calls. The ‘apply’ trap is triggered when a function (the target) is invoked. This allows you to customize how the function is called, including the arguments passed to the function and the value it returns.

How does the ‘handler’ object work in ES6 Proxies?

The ‘handler’ object in ES6 Proxies is an object that defines the behavior of the proxy when an operation is performed on it. It contains methods, known as ‘traps’, that are triggered when corresponding operations are performed on the proxy.

Can I use ES6 Proxies to validate object properties?

Yes, ES6 Proxies can be used to validate object properties. The ‘set’ trap can be used to validate and control the values assigned to properties. If the validation fails, you can throw an error.

How can I use ES6 Proxies for data binding?

ES6 Proxies can be used for data binding by intercepting changes to an object and updating the DOM in real time. The ‘set’ trap can be used to detect when a property is changed, and then update the DOM accordingly. This can be useful for creating reactive user interfaces.

Craig BucklerCraig Buckler
View Author

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

ECMAScript 2015ECMAScript6es6jameshlearn-modernjsmodernjsmodernjs-hubobjectsproxytwo-way data Binding
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week