Advanced Proxy Patterns

This section covers a few more Proxy patterns: observable objects, revocable proxies, and default values.

Observable Object

function observable(obj) {
  const listeners = new Map();

  return new Proxy(obj, {
    set(target, property, value, receiver) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value, receiver);

      if (oldValue !== value && listeners.has(property)) {
        listeners.get(property).forEach((fn) => fn(value, oldValue));
      }

      return result;
    },
    get(target, property, receiver) {
      if (property === "on") {
        return (prop, callback) => {
          if (!listeners.has(prop)) {
            listeners.set(prop, []);
          }
          listeners.get(prop).push(callback);
        };
      }
      return Reflect.get(target, property, receiver);
    },
  });
}

const state = observable({ count: 0 });

state.on("count", (newVal, oldVal) => {
  console.log(`count changed from ${oldVal} to ${newVal}`);
});

state.count = 1;  // Logs: "count changed from 0 to 1"
state.count = 2;  // Logs: "count changed from 1 to 2"

Revocable Proxy

Create a proxy that can be disabled:

const { proxy, revoke } = Proxy.revocable(
  { x: 1 },
  {
    get(target, property) {
      return target[property];
    },
  },
);

console.log(proxy.x); // 1

revoke(); // Disable the proxy

// console.log(proxy.x);  // TypeError: Cannot perform 'get' on a proxy that has been revoked

Default Values

function withDefaults(target, defaults) {
  return new Proxy(target, {
    get(target, property, receiver) {
      const value = Reflect.get(target, property, receiver);
      if (value === undefined && property in defaults) {
        return defaults[property];
      }
      return value;
    },
  });
}

const config = withDefaults(
  { port: 3000 },
  { host: "localhost", port: 8080, debug: false },
);

console.log(config.port);  // 3000 (from target)
console.log(config.host);  // "localhost" (from defaults)
console.log(config.debug);  // false (from defaults)

Performance Considerations

Proxies add overhead to every intercepted operation. Avoid using them:

  • In hot code paths
  • On objects accessed in tight loops
  • When simpler solutions exist

Use a Proxy when what you get from it is worth that overhead. Validation, logging, and reactivity are the usual reasons.

Summary

Feature Purpose
Proxy Intercept and customize object operations
Reflect Perform default operations, mirrors Proxy traps
Traps Handler methods that intercept specific operations
Proxy.revocable() Create a proxy that can be disabled

Common use cases:

  • Validation and type checking
  • Logging and debugging
  • Observable/reactive objects
  • Virtual properties and default values
  • Access control