How JavaScript Proxies Intercept Object Operations

JavaScript Proxies provide a powerful mechanism to wrap target objects and customize their default behavior. Introduced in ECMAScript 6 (ES2015), a Proxy intercepts fundamental internal operations—such as property lookup, assignment, enumeration, and function invocation—allowing developers to redefine how these operations execute. This article explains the mechanics of JavaScript Proxies, how handlers and traps work, the role of the Reflect API, and practical implementation patterns for intercepting object interactions.

Understanding the Core Components: Target, Handler, and Traps

A Proxy is instantiated using the Proxy constructor, which requires two parameters:

const proxy = new Proxy(target, handler);
  1. Target: The original object (an object, array, function, or another proxy) that the Proxy virtualizes.
  2. Handler: An object containing methods (called traps) that define the custom behavior when an operation is performed on the proxy.
  3. Traps: Methods within the handler that correspond to internal ECMAScript engine operations. If a trap is defined, the proxy executes that trap instead of the standard operation. If no trap is defined, the operation falls back to the default behavior on the target.

How Interception Works: Mapping Traps to Internal Methods

JavaScript engines interact with objects via low-level internal methods (e.g., [[Get]], [[Set]], [[Delete]], [[Call]]). A Proxy provides public trap functions that directly intercept these internal methods.

Here are the most common internal methods and their corresponding Proxy traps:

Internal Method Proxy Trap Triggering Operation
[[Get]] get(target, prop, receiver) Reading a property (obj.prop or obj['prop'])
[[Set]] set(target, prop, value, receiver) Writing a property (obj.prop = value)
[[HasProperty]] has(target, prop) The in operator ('prop' in obj)
[[Delete]] deleteProperty(target, prop) The delete operator (delete obj.prop)
[[Call]] apply(target, thisArg, argumentsList) Function call (fn(...args))
[[Construct]] construct(target, argumentsList, newTarget) The new operator (new Cls())

The Role of the Reflect API

When redefining operations within a trap, you often still need to perform the default action. The global Reflect object provides methods that match the exact signatures and behaviors of the Proxy traps.

Using Reflect ensures that default operations maintain their original semantics, correctly forward the receiver (preserving the expected this context), and return appropriate boolean values for success or failure.

const user = { name: "Alex" };

const handler = {
  get(target, prop, receiver) {
    console.log(`Accessing property: ${String(prop)}`);
    return Reflect.get(target, prop, receiver);
  }
};

const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name); 
// Logs: "Accessing property: name"
// Output: "Alex"

Common Interception Patterns

1. Data Validation with the set Trap

The set trap intercepts property writes, allowing you to enforce type safety or value constraints before modifying the target object.

const validator = {
  set(target, prop, value, receiver) {
    if (prop === "age") {
      if (typeof value !== "number" || value <= 0) {
        throw new TypeError("Age must be a positive number.");
      }
    }
    return Reflect.set(target, prop, value, receiver);
  }
};

const profile = new Proxy({}, validator);
profile.age = 25; // Succeeds
// profile.age = -5; // Throws TypeError: Age must be a positive number.

2. Providing Default Values with the get Trap

The get trap can redefine how missing properties are handled, avoiding undefined errors.

const dictionary = { hello: "bonjour" };

const safeDictionary = new Proxy(dictionary, {
  get(target, prop, receiver) {
    if (prop in target) {
      return Reflect.get(target, prop, receiver);
    }
    return `Translation for '${String(prop)}' not found.`;
  }
});

console.log(safeDictionary.hello);   // "bonjour"
console.log(safeDictionary.goodbye); // "Translation for 'goodbye' not found."

3. Intercepting Function Calls with the apply Trap

When wrapping functions, the apply trap intercepts execution, enabling logging, performance profiling, or argument transformation.

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

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

console.log(tracedSum(5, 10)); 
// Logs: "Called with arguments: 5, 10"
// Output: 15

Practical Applications