Ad – 728Γ—90
πŸš€ Advanced JS

JavaScript Proxy and Reflect – Intercepting Object Operations

A Proxy wraps a target object and lets you intercept and customise fundamental operations β€” property reads, writes, deletions, function calls β€” via trap functions. The Reflect API provides corresponding methods for performing the default behaviour, making it easy to intercept an operation, do some work, then forward to the default. Together they enable powerful metaprogramming patterns used by reactive UI frameworks like Vue 3.

⏱️ 21 min read 🎯 Advanced πŸ“… Updated 2026

Proxy Basics

new Proxy(target, handler) creates a proxy for target. The handler object contains traps β€” methods named after the operations they intercept. If a trap is absent, the operation falls through to the target unmodified.

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

const handler = {
  get(target, prop, receiver) {
    console.log(`GET ${String(prop)}`);
    return Reflect.get(target, prop, receiver);  // default behaviour
  },

  set(target, prop, value, receiver) {
    console.log(`SET ${String(prop)} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name);       // triggers get trap
proxy.age = 31;                // triggers set trap
console.log(proxy.age);        // triggers get trap

// The target is also mutated
console.log(target.age);       // 31
β–Ά Output
GET name
Alice
SET age = 31
GET age
31
31

Proxy for Validation

The set trap can enforce type and value constraints on every property write, turning any plain object into a validated schema.

JavaScript
function createValidated(schema) {
  return new Proxy({}, {
    set(target, prop, value) {
      const rule = schema[prop];
      if (!rule) throw new TypeError(`Unknown property: ${String(prop)}`);

      if (rule.type && typeof value !== rule.type) {
        throw new TypeError(`${String(prop)} must be ${rule.type}, got ${typeof value}`);
      }
      if (rule.min !== undefined && value < rule.min) {
        throw new RangeError(`${String(prop)} must be >= ${rule.min}`);
      }
      if (rule.max !== undefined && value > rule.max) {
        throw new RangeError(`${String(prop)} must be <= ${rule.max}`);
      }
      if (rule.pattern && !rule.pattern.test(value)) {
        throw new Error(`${String(prop)} does not match pattern`);
      }

      return Reflect.set(target, prop, value);
    }
  });
}

const user = createValidated({
  name:  { type: 'string', pattern: /^[A-Za-z ]{2,50}$/ },
  age:   { type: 'number', min: 0, max: 150 },
  email: { type: 'string', pattern: /^.+@.+\..+$/ }
});

user.name  = 'Bob Smith';
user.age   = 25;
user.email = 'bob@example.com';
console.log(user.name, user.age, user.email);

try { user.age = -5; } catch (e) { console.error(e.message); }
try { user.name = 123; } catch (e) { console.error(e.message); }
try { user.phone = '555'; } catch (e) { console.error(e.message); }
β–Ά Output
Bob Smith 25 bob@example.com
age must be >= 0
name must be string, got number
Unknown property: phone

Other Useful Traps

JavaScript
const sensitiveData = {
  password: 'secret',
  apiKey:   'abc123',
  name:     'Alice'
};

const secureProxy = new Proxy(sensitiveData, {
  // Intercept property read
  get(target, prop) {
    if (prop === 'password' || prop === 'apiKey') {
      return '***REDACTED***';
    }
    return Reflect.get(target, prop);
  },

  // Intercept `in` operator
  has(target, prop) {
    console.log(`Checking: ${String(prop)} in proxy`);
    return Reflect.has(target, prop);
  },

  // Intercept delete operator
  deleteProperty(target, prop) {
    if (prop === 'password') {
      throw new Error('Cannot delete password field');
    }
    return Reflect.deleteProperty(target, prop);
  },

  // Intercept Object.keys / for...in
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(k => k !== 'apiKey');
  }
});

console.log(secureProxy.password);        // ***REDACTED***
console.log(secureProxy.name);            // Alice
console.log('password' in secureProxy);   // true (but value is hidden)
console.log(Object.keys(secureProxy));    // ['password', 'name'] (no apiKey)
try { delete secureProxy.password; } catch (e) { console.error(e.message); }
β–Ά Output
***REDACTED***
Alice
Checking: password in proxy
true
['password', 'name']
Cannot delete password field

The Reflect API

Reflect methodEquivalent operationUsed in trap
Reflect.get(t, p, r)t[p]get
Reflect.set(t, p, v, r)t[p] = vset
Reflect.has(t, p)p in thas
Reflect.deleteProperty(t, p)delete t[p]deleteProperty
Reflect.apply(t, th, a)t.apply(th, a)apply
Reflect.construct(t, a, nt)new t(...a)construct
Reflect.ownKeys(t)Object.getOwnPropertyNames + SymbolsownKeys
πŸ’‘
Always use Reflect inside traps

Inside a proxy trap, always forward to the corresponding Reflect method to perform the default behaviour. Doing the equivalent operation on target directly (target[prop]) bypasses getter/setter semantics and can break object invariants. Reflect methods also return the correct boolean for traps like set and deleteProperty.

Default Values and Function Call Trapping

JavaScript
// Default value proxy (like Python's defaultdict)
function withDefaults(target, defaultValue) {
  return new Proxy(target, {
    get(obj, prop) {
      return prop in obj ? obj[prop] : defaultValue;
    }
  });
}

const scores = withDefaults({}, 0);
scores.Alice = 10;
scores.Bob   = 20;
console.log(scores.Alice);    // 10
console.log(scores.Charlie);  // 0 (default β€” not set)

// Function call trap
function multiply(a, b) { return a * b; }

const trackedMultiply = new Proxy(multiply, {
  apply(target, thisArg, args) {
    console.log(`Called with args: ${args}`);
    const result = Reflect.apply(target, thisArg, args);
    console.log(`Result: ${result}`);
    return result;
  }
});

trackedMultiply(3, 4);
trackedMultiply(7, 8);
β–Ά Output
10
0
Called with args: 3,4
Result: 12
Called with args: 7,8
Result: 56

Proxy.revocable()

Proxy.revocable(target, handler) returns { proxy, revoke }. After calling revoke(), any operation on the proxy throws a TypeError. Useful for temporary access tokens or capability objects.

JavaScript
function createTemporaryAccess(data, ttlMs) {
  const { proxy, revoke } = Proxy.revocable(data, {});
  setTimeout(revoke, ttlMs);    // auto-revoke after TTL
  return proxy;
}

const tempAccess = createTemporaryAccess({ secret: 'TOKEN_XYZ' }, 500);

console.log(tempAccess.secret);  // 'TOKEN_XYZ'

setTimeout(() => {
  try {
    console.log(tempAccess.secret);  // throws after 500ms
  } catch (e) {
    console.error('Access revoked:', e.message);
  }
}, 600);
β–Ά Output
TOKEN_XYZ
Access revoked: Cannot perform 'get' on a proxy that has been revoked
ℹ️
Vue 3 Reactivity Uses Proxy

Vue 3's reactivity system (reactive(), ref()) wraps your state objects in Proxies with get and set traps. The get trap tracks which component is reading the value; the set trap triggers re-renders when the value changes. This is fundamentally more capable than Vue 2's Object.defineProperty approach, which couldn't detect property additions or deletions.

Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Build a createObservable(obj) function that:

  • Returns a proxied version of obj.
  • Accepts subscriber callbacks via observable.on(prop, callback).
  • Calls callback(newValue, oldValue) whenever prop changes.
  • Supports nested objects (deep proxy).
  • Test: observe a user object and log changes to name and email.

πŸ”₯ Challenge Exercise

Implement a lightweight reactive(obj) system inspired by Vue 3: when a property changes, automatically re-run any "effect" functions that previously read that property. Use a global activeEffect variable to track the currently-running effect, a get trap to subscribe it, and a set trap to trigger subscribers. Demonstrate with a computed display name that auto-updates when firstName or lastName changes.

Interview Questions

  • What is a Proxy in JavaScript and what problem does it solve?
  • Name five proxy traps and describe what operation each intercepts.
  • Why should you use Reflect methods inside proxy traps instead of operating on the target directly?
  • What is Proxy.revocable() and when would you use it?
  • How does Vue 3 use Proxy for its reactivity system?

πŸ“‹ Summary

  • new Proxy(target, handler) intercepts operations on the target via trap methods.
  • Common traps: get, set, has, deleteProperty, apply, ownKeys.
  • Always forward to Reflect inside traps to maintain correct semantics.
  • Use Proxy for validation, logging, default values, read-only access, and observability.
  • Proxy.revocable() creates proxies that can be disabled, enabling time-limited access tokens.
  • Vue 3's reactivity system is built on Proxy β€” a real-world example of production metaprogramming.

Frequently Asked Questions

Can Proxy intercept operations on private class fields? +

No. Private class fields (#field) are stored in an internal slot, not as regular object properties. Proxy traps only intercept regular property access. A get trap on a proxy of a class instance will not see reads to private fields β€” those bypass the proxy entirely.

What is the difference between Proxy get trap and Object.defineProperty getter? +

Object.defineProperty getters are tied to a specific named property on a specific object β€” you must define one per property. A Proxy get trap intercepts all property reads on the target, including dynamic and unknown property names. This makes Proxy much more flexible for generic patterns like default values or logging all accesses.

Are Proxy and Reflect performance overhead? +

Yes β€” proxy traps add overhead to every intercepted operation. Modern JS engines have optimised this considerably, but Proxies are still slower than direct property access. In hot code paths, profile before proxying. For UI reactivity (Vue, MobX), the overhead is negligible compared to rendering costs.