What are JavaScript Proxies and how they work

What are JavaScript Proxies and how they work

One of the most powerful, but not often seen or used, features of JavaScript are proxies. Proxies were introduced in ECMAScript 6 and they allow developers to intercept and redefine the built-in operations on objects.

In this article I will break down what proxies are, how to create them and cover a few of the downsides that can come with using them too much.

What is a Proxy?

A JavaScript Proxy gives you the ability to define custom behavior for fundamental built-in operations on objects such as accessing properties, performing assignments on object properties and invoking functions.

Proxy definition takes in two arguments:

1. target - The object (or function) that you want to wrap.

2. handler - An object that contains various methods that will intercept the built-in operations.

And here's an example of what that would look like if we were intercepting a basic get call on an object.

const target = { name: "Alice" };

const handler = {
  get: (obj, prop) => {
    if (prop in obj) {
      return obj[prop];
    } else {
      return `Property "${prop}" does not exist.`;
    }
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name);  // Output: Alice
console.log(proxy.age);   // Output: Property "age" does not exist.

The get property above is triggered whenever one of the objects properties is accessed, allowing you to intercept and modify the output if needed.

In the example above, if a property is accessed but does not exist in the target object, then we can override the value that is returned back to the caller.

Use cases

Accessing properties is just one use case however. You can also intercept the process of setting a properties values as well.

Validation and sanitization

Proxies are great for intercepting property modifications in order to ensure the proper values. Here is an example of that functionality:

const validator = {
  set: (obj, prop, value) => {
    if (prop === 'age') {
      if (typeof value !== 'number') {
        throw new Error('Age must be a number');
      }
      if (value < 0 || value > 150) {
        throw new Error('Invalid age');
      }
    }
    obj[prop] = value;
    return true;
  }
};

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

person.age = 25;  // Valid age
person.age = '30';  // Throws error: Age must be a number

In this particular example, if a user were to provide a non-numeric value as the age, the Proxy would throw an error back to the caller. If the value is correct however, it would return that same value back and work as normal.

Logging

Another useful use case for proxies, is in automatic logging when properties are modified.

const logger = {
  get: (obj, prop) => {
    console.log(`Accessed property: ${prop}`);
    return obj[prop];
  },
  set: (obj, prop, value) => {
    console.log(`Modified property: ${prop} with value: ${value}`);
    obj[prop] = value;
    return true;
  }
};

const user = new Proxy({ name: "Alice" }, logger);

console.log(user.name);  // Logs: Accessed property: name
user.name = "Bob";       // Logs: Modified property: name with value: Bob

In this particular example we're just logging the change back to the console. However in a real world application, a developer could potentially call a logging API and track this specific object property change.

The biggest benefit here is the potential reusability of the handler with multiple target objects. Adding logging to a new object would be as simple as creating a new Proxy and assigning the handler to the desired target.

Function proxies

Proxies are not solely limited to objects. They can also be used on JavaScript functions in order to modify their behavior.

Here is an example of function proxy in action:

function sum(a, b) {
  return a + b;
}

const functionProxy = new Proxy(sum, {
  apply: (target, thisArg, argumentsList) => {
    console.log(`Called with arguments: ${argumentsList}`);
    return target(...argumentsList);
  }
});

console.log(functionProxy(3, 4));  // Logs: Called with arguments: 3,4 -> 7

In this example you can see that the target of the Proxy is the function named sum. And the handler method passed in is intercepting the apply operation, which gets triggered every time that the function is called.

In this example, we're just logging the argument list to the console. But in a real-world application, you could potentially validate, sanitize or log the data being passed in before it is used by the function.

Performance

Proxies are incredibly useful, however, they don't come for free. They do have a slight performance trade off. By default, JavaScript does a pretty good job at optimization on object operations.

But if we override that behavior, we of course introduce some extra overhead. This is expected and unless you're doing some heavy duty object manipulation in your proxies, you shouldn't see a noticeable degradation.

However, for that reason, you probably shouldn't be creating Proxies for every single object in your applications, as traditional validation and sanitization methods are still a valid approach.

Conclusion

JavaScript proxies open up a wide range of possibilities for controlling object behavior, from validation and logging to lazy-loading data and implementing design patterns like the Observer. However, it's crucial to consider the performance implications, particularly in performance-sensitive applications.

Walter G. author of blog post
Walter Guevara is a Computer Scientist, software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20 years.

Get the latest programming news directly in your inbox!

Have a question on this article?

You can leave me a question on this particular article (or any other really).

Ask a question

Community Comments

No comments posted yet

Add a comment