JavaScript Proxy: A Practical Guide

In JavaScript, a Proxy object is an object that wraps another object (target) and intercepts the fundamental operations of the target object. The Proxy object allows you to create an object that can be used in place of the original object, but which may redefine fundamental Object operations like getting, setting, and defining properties.

Proxies are commonly used to log property accesses, validate, format or sanitize inputs, and so on. They can also be used for implementing features like data validation, caching, logging/debugging, security checks or other custom behavior that needs to be applied consistently across multiple objects.

Applications

In JavaScript, a Proxy object can be used in a variety of use cases. Some common use cases for a proxy object include:

  1. Logging property accesses: Proxies can be used to log when properties are accessed on an object, which can be useful for debugging and performance analysis.

  2. Validating inputs: Proxies can be used to validate inputs to functions or methods by intercepting read/write operations on the input object and checking that they conform to a specified schema.

  3. Formatting or sanitizing inputs: Proxies can also be used to format or sanitize inputs by intercepting read/write operations on the input object and transforming them according to some predefined rules.

  4. Caching: Proxies can be used to cache expensive computations by intercepting read/write operations on the target object and returning cached results instead of recomputing them each time.

  5. Security checks: Proxies can be used to enforce security policies by intercepting read/write operations on sensitive objects and checking that they conform to some predefined security rules.

  6. Metaprogramming: Proxies enable you to write programs that can create other programs, which is known as metaprogramming. This allows you to create more flexible and dynamic code that adapts itself at runtime based on changing conditions.

Syntax

A JavaScript Proxy is an object that wraps another object (target) and intercepts the fundamental operations of the target object. The syntax for declaring a proxy object is let proxy = new Proxy(target, handler).

  • target: The object to wrap and can be anything, including functions.

  • handler: The object that defines the list of methods that provide property access.

In this syntax, the target parameter is the object to wrap and can be any type of object. The handler parameter is an object that defines a set of traps (methods) that intercept fundamental operations on the target object. When an operation is intercepted by a trap, it can be handled in a custom way defined by the developer.

Basic example

Here is an example of creating a simple JavaScript Proxy:

let target = {
  name: "Julian",
  age: 40
};

let handler = {
  get: function(target, prop) {
    if (prop === "age") {
      return target[prop] + " years old";
    } else {
      return target[prop];
    }
  }
};

let proxy = new Proxy(target, handler);

console.log(proxy.name); // Output: Julian
console.log(proxy.age); // Output: 40 years old

In this example, we create a target object with two properties. We then create a handler object with a get method that intercepts property access and modifies the value of the age property. Finally, we create a proxy object that wraps the target object using the handler. When we access the properties of the proxy, it returns either the original value or the modified value based on our implementation in the handler.

Manipulating DOM nodes

Proxies can be used to manipulate DOM. The purpose of using proxies for manipulating the DOM is to enable developers to intercept and redefine fundamental operations for a DOM element. Proxies can be used to add extra logging of relevant information, encapsulate the original object, and allow access through only certain methods. Additionally, proxies can be used in combination with a virtual DOM to build your own framework for manipulating the DOM. By using Proxy, the model changes can be controlled and responses can be made automated without using different frameworks.

Here is an example code snippet that demonstrates how to create a proxy for manipulating DOM nodes:

const domElement = document.querySelector('#test-element');

const domProxy = new Proxy(domElement, {
  get(target, prop) {
    if (prop === 'textContent') {
      return target.textContent.toUpperCase();
    } else if (prop === 'style') {
      return new Proxy(target.style, {
        get(styleTarget, styleProp) {
          return styleTarget[styleProp];
        },
        set(styleTarget, styleProp, value) {
          styleTarget[styleProp] = value;
          target.setAttribute('style', target.getAttribute('style'));
          return true;
        }
      });
    } else {
      return target[prop];
    }
  }
});

domProxy.textContent = 'Hello World'; // Sets textContent to "HELLO WORLD"
domProxy.style.color = 'violet'; // Sets the color of the element to violet

In this example, we create a domElement variable that references an existing DOM element. We then create a domProxy object that wraps the domElement using a get trap. The get trap intercepts property access and returns either the original value or modified value based on our implementation in the handler. In this case, we modify the textContent property to be uppercase and wrap the style property in another proxy object that updates the inline style attribute when it is modified. Finally, we use the domProxy object to manipulate properties of the underlying DOM element.

Validation

You can also use JavaScript Proxy to validate objects. For example:

const validator = {
  set(obj, prop, value) {
    if (prop === 'name') {
      if (typeof value !== 'string') {
        throw new TypeError('Name must be a string');
      }
    } else if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('Age must be an integer');
      }
      if (value < 0 || value > 150) {
        throw new RangeError('Age must be between 0 and 150');
      }
    } else if (prop === 'email') {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(value)) {
        throw new Error('Invalid email address');
      }
    }

    obj[prop] = value;
    return true;
  }
};

const person = new Proxy({}, validator);

person.name = 'Julian'; // Sets name to "Julian"
person.age = 40; // Sets age to 40
person.email = 'julian@example.com'; // Sets email to "julian@example.com"

person.name = {}; // Throws a TypeError: Name must be a string
person.age = -1; // Throws a RangeError: Age must be between 0 and 150
person.email = 'invalid-email'; // Throws an Error: Invalid email address

In this example, we create a validator object with a set method that intercepts property assignment. The set method checks whether the property being assigned is name, age, or email, and validates the value based on the type of the property. If the value is invalid, it throws either a TypeError, RangeError, or generic Error. Finally, we create a proxy object using the Proxy constructor and pass in the validator object as the handler. When we assign values to properties of the proxy object, it goes through the validation process defined in our handler.

Conclusion

In conclusion, proxies provide a powerful mechanism for creating custom behavior around existing objects in JavaScript. They can help improve code quality by providing a simple and intuitive way to define schemas for data validation and type checking, as well as enabling more advanced features like caching, security checks, and metaprogramming.