Creating a Basic Proxy

A Proxy is created using the Proxy constructor, which takes two arguments: the target object and a handler object that defines the custom behaviors.

const target = {
  name: "Alice",
  age: 30
};

const handler = {
  get: function(obj, prop) {
    if (prop === 'age') {
      return `Age is ${obj[prop]} years old`;
    }
    return obj[prop];
  },
  set: function(obj, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError("Age must be a number");
    }
    obj[prop] = value;
    return true;
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // Alice
console.log(proxy.age);  // Age is 30 years old
proxy.age = 35;
console.log(proxy.age);  // Age is 35 years old
proxy.age = "thirty";    // Throws TypeError

Common Proxy Traps

Proxy objects use "traps" to intercept operations. Each trap corresponds to an internal method of the target object. Here are some of the most commonly used traps:

TrapDescription
getIntercepts property access
setIntercepts property assignment
hasIntercepts in operator
deletePropertyIntercepts delete operator
applyIntercepts function calls
constructIntercepts new operator

Example: Using has and deleteProperty Traps

const target = {
  name: "Bob",
  role: "Developer"
};

const handler = {
  has(target, prop) {
    console.log(`Checking if property "${prop}" exists`);
    return prop in target;
  },
  deleteProperty(target, prop) {
    console.log(`Deleting property "${prop}"`);
    delete target[prop];
    return true;
  }
};

const proxy = new Proxy(target, handler);

console.log("name" in proxy); // true
console.log("age" in proxy);  // false
delete proxy.role;
console.log(proxy.role);      // undefined

Proxy with Functions: The apply and construct Traps

Proxies can be applied to functions, allowing you to intercept calls and constructor invocations.

const target = function (x, y) {
  return x + y;
};

const handler = {
  apply: function(target, thisArg, argumentsList) {
    console.log("Function called with:", ...argumentsList);
    return target.apply(thisArg, argumentsList);
  },
  construct: function(target, argumentsList, newTarget) {
    console.log("Function constructed with:", ...argumentsList);
    return new target(...argumentsList);
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy(2, 3));        // 5, logs: Function called with: 2 3
console.log(new proxy(2, 3));    // 5, logs: Function constructed with: 2 3

Best Practices for Using Proxies

  • Use for validation and access control: Proxies are ideal for enforcing data integrity and permissions.
  • Avoid performance overhead: While powerful, proxies can introduce performance costs. Use them judiciously.
  • Be mindful of object identity: Proxies create a new object reference, which can affect comparisons and caching.
  • Use with caution in production code: Some environments (e.g., minifiers or transpilers) may not support Proxies well.

Practical Use Case: Caching with Proxies

A Proxy can be used to implement a simple caching mechanism for an object's properties.

const cache = new Map();
const target = {
  fetch: function (key) {
    console.log(`Fetching data for key: ${key}`);
    return `Value for ${key}`;
  }
};

const handler = {
  get(obj, prop) {
    if (prop === "fetch") {
      return function (key) {
        if (cache.has(key)) {
          return cache.get(key);
        }
        const value = obj[prop](key);
        cache.set(key, value);
        return value;
      };
    }
    return Reflect.get(...arguments);
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.fetch("data1")); // Fetching data for key: data1
console.log(proxy.fetch("data1")); // From cache
console.log(proxy.fetch("data2")); // Fetching data for key: data2

Learn more with useful resources