An Introduction to JavaScript Proxies
Published: 2023-05-30 23:32:52

by [Craig Buckler](https://blog.openreplay.com/authors/craig-buckler)

<blockquote><em>A "proxy" intercepts messages between you and a particular system. You've possibly encountered the term "proxy server". It's a device between your web browser and a web server that can examine or change requests and responses. They often cache assets, so downloads are faster. JavaScript proxies arrived in ES2015. A proxy sits between an object and the code that uses it. You can use them for meta-programming operations such as intercepting property updates. This article will teach you everything about proxies, so you can use them on your own.</em></blockquote>

Consider this simple JavaScript object literal:

```javascript
const target = {
  a: 1,
  b: 2,
  c: 3,
  sum: function() { return this.a + this.b + this.c; }
};
```

You can examine and update the numeric properties and sum their values:

```javascript
console.log( target.sum() );  // 6

target.a = 10;
console.log( target.a );      // 10
console.log( target.sum() );  // 15
```

JavaScript is a forgiving language, and it lets you make invalid updates which could cause problems later:

```javascript
target.a = 'not a number!';
delete target.b;
target.c = undefined;
target.d = 'new property';

target.sum = () => 'hello!';

console.log( target.sum() );  // hello!
```

Note: you can prevent some unwanted actions using `Object` methods such as [`.defineProperty()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), [`.preventExtensions()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions), and [`.freeze()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) but they're blunt tools and won't prevent all updates.

A proxy object can intercept changes to a **target** object. It's defined with a **handler** that sets **trap** functions called when certain actions occur (get, set, delete, etc.) to change the behavior of the **target** object.

The following handler intercepts all `set` property operations such as `myObject.a = 999`. It's passed the `target` object, the `property` name as a string, and the `value` to set:

```javascript
const handler = {

  // set property
  set(target, property, value) {

    // is value numeric?
    if (typeof value !== 'number' || isNaN(value)) {
      throw new TypeError(`Invalid value ${ value }`);
    }

    return Reflect.set(...arguments);

  }

}
```

The function throws an error when the passed `value` is `Nan` or not numeric. If it's valid, [`Reflect`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Reflect) executes the default object behavior -- in this case to [`set`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set) the target object's property (all `Reflect` method parameters are identical to the proxy's handler function). You could use `target[property] = value;` instead but `Reflect` makes some operations easier to manage.

You can now create a new `Proxy` object by passing the `target` and `handler` objects to its constructor:

```javascript
const proxy = new Proxy(target, handler);
```

Now use the `proxy` object instead of `target` -- the same properties and methods work as before:

```javascript
console.log( proxy.sum() );  // 6

proxy.a = 10;
console.log( proxy.a );      // 10
console.log( proxy.sum() );  // 15
```

But setting a non-numeric value raises a `TypeError`, and the program halts:

```javascript
proxy.a = 'xxx'; // TypeError: Invalid value xxx
```

The following code improves the Proxy `handler` further:

* the `set` trap adds another check to ensure a property already exists and is numeric. It throws a `ReferenceError` when calling code attempts to set unsupported property names (anything other than `a`, `b`, or `c`) or override the `sum()` function.

* a new `deleteProperty` trap throws a `ReferenceError` when calling code attempts to delete any property, e.g., `delete proxy.a`.

* a new `get` trap throws a `ReferenceError` when calling code attempts to get a property or call a method that doesn't exist.


```javascript
const handler = {

  // set property
  set(target, property, value) {

    // is it a valid property?
    if (
      !Reflect.has(target, property) || 
      typeof Reflect.get(target, property) !== 'number'
    ) {
      throw new ReferenceError(`Invalid property ${ property }`);
    }

    // is value numeric?
    if (typeof value !== 'number' || isNaN(value)) {
      throw new TypeError(`Invalid value ${ value }`);
    }

    return Reflect.set(...arguments);

  },

  // delete property
  deleteProperty(target, property) {
    throw new ReferenceError(`Cannot delete ${ property }`);
  },

  // get property
  get(target, property) {

    // is it a valid property?
    if (!Reflect.has(target, property)) {
      throw new ReferenceError(`Invalid property ${ property }`);
    }

    return Reflect.get(...arguments);

  }

}
```

Examples:

```javascript
proxy.a = 10;               // successful
proxy.a = null;             // TypeError: Invalid value null
proxy.d = 99;               // ReferenceError: Invalid property d
proxy.sum = () => 'hello!'; // ReferenceError: Invalid property sum
delete proxy.a;             // ReferenceError: Cannot delete a
console.log( proxy.e );     // ReferenceError: Invalid property e
```

Validating types is not the most interesting use of proxies, and should you require type support, perhaps you should consider [TypeScript](https://www.typescriptlang.org/)! We'll examine a more advanced example below.


## Proxy trap types

A `Proxy` allows you to intercept actions on a `target` object. The **handler** object defines **trap** functions. In most cases, you'll be using `get` or `set,` but the following traps support more advanced use:

* [`construct(target, argList)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct)

  Called when creating an object with the `new` operator.

* [`get(target, property)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get)

  Called when examining a property or running a method.

* [`set(target, property, value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set)

  Called when setting a property with a value. Returning `false` throws a `TypeError` exception.

* [`defineProperty(target, property, descriptor)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/defineProperty)

  Called when using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) to create or update a property. It must return `true` (successfully defined) or `false` (could not be defined).

* [`deleteProperty(target, property)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/deleteProperty)

  Called when deleting a property. It must return either `true` (deleted) or `false` (not deleted).

* [`apply(target, thisArg, argList)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply)

  Called when executing a **target** object, which is a function (it's not called for functions defined as object methods).

* [`has(target, property)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys)

  Called when using a static method such as `in`, e.g., `'p' in object`. It must return either `true` (defined) or `false` (not defined).

* [`ownKeys(target)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys)

  Called when using [`Object.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys). It must return an array of enumerable string-keyed property names, such as `["a", "b", "c"]`.

* [`isExtensible(target)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/isExtensible)

  Called when using [`Object.isExtensible()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible) to check whether the object permits new properties. It must return `true` or `false`.

* [`preventExtensions(target)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/preventExtensions)

  Called when using [`Object.preventExtensions()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions) to stop the object permitting new properties. It must return `true` or `false`.

* [`getOwnPropertyDescriptor(target, property)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor)

  Called when using [`Object.getOwnPropertyDescriptor()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) to return an object describing the configuration of a specific property. It must return an appropriate object with `value`, `writable`, `configurable`, `enumerable`, `get`, and `set` properties.

* [`getPrototypeOf(target)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getPrototypeOf)

  Called when getting the object prototype using [`Object.getPrototypeOf()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf).

* [`setPrototypeOf(target, prototype)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/setPrototypeOf)

  Called when setting the object prototype using [`Object.setPrototypeOf()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf).


All traps have an associated [`Reflect()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect) method with identical parameters, so it's not necessary to create your own implementation code when you require the default behavior. For example:

```javascript
const handler = {

  // trap property descriptor
  getOwnPropertyDescriptor(target, property) {

    console.log(`examining property ${ property }`);

    return Reflect.getOwnPropertyDescriptor(...arguments);

  }

};
```



<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>







## Two-way data binding with a Proxy

Data binding synchronizes two or more disconnected objects. In this example, updating a form field changes a JavaScript object's property and vice versa.

[**View the demonstration Codepen...**](https://codepen.io/craigbuckler/pen/eYLaBGr?editors=1011)

*(Click the SUBMIT button to view the object's properties in the Codepen console.)*

The HTML has a form with the ID `myform` and three fields:

```html
<form id="myform" action="get">

  <input name="name" type="text" />
  <input name="email" type="email" />
  <textarea name="comments"></textarea>

</form>
```

The JavaScript code creates a `myForm` object bound to this form by passing its node to a `FormBinder()` factory function:

```javascript
// form reference
const myformElement = document.getElementById('myform');

// create 2-way data form
const myForm = FormBinder(myformElement);
```

From that point onwards, the object's properties return the current state of the associated field:

```javascript
myForm.name;      // value of name field
myForm.email;     // value of email field
myForm.comments;  // value of comments field
```

You can update the same properties, and the associated form field will change accordingly:

```javascript
myForm.name = 'Some Name'; // update name field
```

![live field update](https://blog.openreplay.com/images/an-introduction-to-javascript-proxies/images/VoPtf0U.png)


The implementation defines a `FormBind` class. The constructor examines all field elements in the passed form, and if they have a `name` attribute, it sets a property of that name to the field's current value. A private `#Field` object also stores the field's name and DOM node for later use.

```javascript
// form binding class
class FormBind {

  #Field = {};

  constructor(form) {

    // initialize object properties
    const elements = form.elements;
    for (let f = 1; f < elements.length; f++) {

      const field = elements[f], name = field.name;
      if (name) {
        this[name] = field.value;
        this.#Field[name] = field;
      }

    }
```

An `input` event handler triggers whenever a form field changes. It checks whether the `#Field` reference exists and updates the associated property with its current value.

```javascript
    // form change events
    form.addEventListener('input', e => {

      const name = e.target.name;
      if (this.#Field[ name ]) {
        this[ name ] = this.#Field[ name ].value;
      }

    });
```

An `updateValue()` method is then defined, which updates both the object property and the HTML field when passed a valid `property` and `newValue`:

```javascript
  // update property and field
  updateValue(property, newValue) {

    if (this.#Field[ property ]) {

      this[ property ] = newValue;
      this.#Field[ property ].value = newValue;
      return true;

    }

    return false;

  }

}
```

To call this method, a Proxy handler defines a single `set` trap that intercepts a property update:

```javascript
// form proxy traps
const FormProxy = {

  // intercept set
  set(target, property, newValue) {
    return target.updateValue(property, newValue);
  }

};
```

A proxy factory function then provides an easy way to create an object which is bound to an HTML form:

```javascript
// form 2-way data binder
function FormBinder(form) {
  return form ? new Proxy(new FormBind(form), FormProxy) : undefined;
}


// form node
const myformElement = document.getElementById('myform');

// create 2-way data form
const myForm = FormBinder(myformElement);

myForm.name = "Some Name";
```

While this is not production-level code, it illustrates the usefulness of JavaScript Proxies. If you want to develop it further, you can add further code to handle the following:

* unusual field names which would not be valid property names, such as `my-name` or `my.name`
* checkbox, radio, and select fields, and
* dynamic HTML DOM updates which add or remove form fields.


## Conclusion

Proxy support is available in all modern browsers and JavaScript runtimes, including Node.js and Deno. They'll only be a problem if you have to support Internet Explorer 11 since there's no way to polyfill or transpile ES6 proxy code to ES5.

Proxies won't be necessary for all your applications, but they provide some interesting opportunities for metaprogramming. You can write programs that analyze or transform other programs or even modify themselves while executing.