JavaScript Proxy Object: The Most Underused Feature in the Language

JavaScript Proxy Object: The Most Underused Feature in the Language

JavaScript Proxy has been in the language since ES2015 and most developers have never used it for anything practical. It intercepts fundamental object operations — property access, assignment, deletion, function calls — and lets you add behaviour transparently. Vue 3’s reactivity system is built on Proxy. MobX uses it. Immer uses it. Here’s why they did.

TL;DR: new Proxy(target, handler) wraps an object and intercepts operations via handler traps. get, set, has, deleteProperty, and apply are the five traps that cover 90% of use cases. Each returns a transparent wrapper — callers never know they’re talking to a Proxy.

Use Case 1: Runtime Validation

function createValidated(schema) {
  return new Proxy({}, {
    set(target, prop, value) {
      const validator = schema[prop];
      if (!validator) throw new Error(`Unknown property: ${prop}`);
      if (!validator(value)) throw new TypeError(`Invalid value for ${prop}: ${value}`);
      target[prop] = value;
      return true;
    }
  });
}

const user = createValidated({
  name: (v) => typeof v === 'string' && v.length > 0,
  age:  (v) => Number.isInteger(v) && v >= 0 && v <= 150,
  email: (v) => /^[^@]+@[^@]+\.[^@]+$/.test(v),
});

user.name = 'Alice';         // OK
user.age  = 30;              // OK
user.age  = -5;              // TypeError: Invalid value for age
user.role = 'admin';         // Error: Unknown property: role

Use Case 2: Observable State (Vue 3 Pattern)

function observable(obj, onChange) {
  return new Proxy(obj, {
    set(target, prop, value) {
      const old = target[prop];
      target[prop] = value;
      if (old !== value) onChange(prop, old, value);
      return true;
    },
    deleteProperty(target, prop) {
      const old = target[prop];
      delete target[prop];
      onChange(prop, old, undefined);
      return true;
    }
  });
}

const state = observable({ count: 0, name: 'Alice' }, (prop, old, next) => {
  console.log(`${prop}: ${old} -> ${next}`);
  document.getElementById(prop)?.textContent = next; // Auto-update DOM
});

state.count++;  // count: 0 -> 1
state.name = 'Bob'; // name: Alice -> Bob
// No framework needed for simple reactive state

Use Case 3: Memoization Proxy

function memoize(fn) {
  const cache = new Map();
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        console.log('Cache hit', key);
        return cache.get(key);
      }
      const result = target.apply(thisArg, args);
      cache.set(key, result);
      return result;
    }
  });
}

const expensiveFn = memoize((n) => {
  // Simulate heavy computation
  return Array.from({length: n}, (_, i) => i).reduce((a, b) => a + b, 0);
});

expensiveFn(1000); // Computed
expensiveFn(1000); // Cache hit — instant

Use Case 4: Negative Array Indexing (Python-Style)

function negativeArray(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      const index = Number(prop);
      if (!isNaN(index) && index < 0) {
        return target[target.length + index]; // -1 = last, -2 = second-to-last
      }
      return Reflect.get(target, prop);
    }
  });
}

const arr = negativeArray([1, 2, 3, 4, 5]);
arr[-1]; // 5 (last)
arr[-2]; // 4
arr[0];  // 1 (positive indices still work)
arr.length; // 5 (all other operations work normally)

Use Case 5: Auto-Logging / Tracing

function trace(obj, label = 'object') {
  return new Proxy(obj, {
    get(target, prop) {
      const val = target[prop];
      if (typeof val === 'function') {
        return function(...args) {
          console.log(`[${label}] ${String(prop)}(${args.map(a => JSON.stringify(a)).join(', ')})`);
          const result = val.apply(target, args);
          console.log(`[${label}] ${String(prop)} returned:`, result);
          return result;
        };
      }
      console.log(`[${label}] get ${String(prop)} =`, val);
      return val;
    },
    set(target, prop, value) {
      console.log(`[${label}] set ${String(prop)} =`, value);
      target[prop] = value;
      return true;
    }
  });
}

const api = trace({ users: [], addUser: (u) => api.users.push(u) }, 'UserAPI');
api.addUser({ name: 'Alice' }); // [UserAPI] addUser({"name":"Alice"})

JavaScript Proxy Cheat Sheet

  • get trap: intercept property reads and method calls
  • set trap: intercept property assignments for validation/reactivity
  • apply trap: intercept function calls for memoization/logging
  • has trap: intercept in operator
  • deleteProperty trap: intercept delete operations
  • ✅ Always use Reflect.* as fallback in traps for correct default behaviour
  • ❌ Proxy has overhead — don't use for hot paths with millions of operations/second
  • ❌ Cannot proxy primitive values — only objects and functions

Proxy works naturally with the WeakMap memory management patterns — store Proxy handler state in WeakMap to avoid memory leaks when proxied objects are garbage collected. The V8 JIT guide explains why Proxy has performance overhead — the JIT can't optimize through traps as easily as direct property access. External reference: MDN Proxy documentation.

Master advanced JavaScript patterns

View Course on Udemy — Hands-on video course covering every concept in this post and more.

Sponsored link. We may earn a commission at no extra cost to you.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply