DNA⚡ JavaScriptProxy, Reflect & Metaprogramming
👑ApexJavaScriptProxyMetaprogrammingAdvanced

Proxy, Reflect & Metaprogramming

Proxy lets you intercept fundamental operations on objects. Reflect provides the default behavior. Together they enable reactive systems, validation layers, and observable state.

Proxy, Reflect & Metaprogramming

Proxy and Reflect are the metaprogramming layer of JavaScript. Every reactive framework — Vue 3's reactivity, MobX observables, Immer's drafts — is built on Proxy. Understanding these primitives means understanding how modern state management actually works under the hood.

Proxy Fundamentals

A Proxy wraps an object and intercepts operations through traps:

const handler = {
  get(target, property, receiver) {
    console.log(`Reading ${String(property)}`);
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`Writing ${String(property)} = ${value}`);
    return Reflect.set(target, property, value, receiver);
  },
};
 
const user = new Proxy({ name: 'Alex', age: 30 }, handler);
user.name;       // logs: Reading name → "Alex"
user.age = 31;   // logs: Writing age = 31

Available Traps

TrapInterceptsExample Operation
getProperty readobj.prop, obj[key]
setProperty writeobj.prop = value
hasin operator'prop' in obj
deletePropertydeletedelete obj.prop
ownKeysKey enumerationObject.keys(obj)
applyFunction callfn()
constructnew operatornew Fn()
getPrototypeOfPrototype accessObject.getPrototypeOf(obj)
definePropertyProperty definitionObject.defineProperty(obj, ...)

Reflect: The Default Behavior API

Reflect provides the default behavior for every proxy trap. Always use Reflect inside traps instead of direct operations:

const handler = {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    return Reflect.set(target, prop, value, receiver);
  },
};

Why Reflect instead of direct access? Reflect methods:

  • Return booleans for success/failure (set, defineProperty, deleteProperty)
  • Properly handle prototype chains via receiver
  • Mirror every proxy trap 1:1
  • Are the correct forwarding mechanism for invariant-preserving proxies

Reactive State (Vue 3 Pattern)

const targetMap = new WeakMap();
let activeEffect = null;
 
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);
      const result = Reflect.get(target, key, receiver);
      return typeof result === 'object' && result !== null
        ? reactive(result)
        : result;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) trigger(target, key);
      return result;
    },
  });
}
 
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let deps = depsMap.get(key);
  if (!deps) depsMap.set(key, (deps = new Set()));
  deps.add(activeEffect);
}
 
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const deps = depsMap.get(key);
  if (deps) deps.forEach(effect => effect());
}
 
function watchEffect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

Usage mirrors Vue 3's composition API:

const state = reactive({ count: 0, name: 'Alex' });
 
watchEffect(() => {
  console.log(`Count is: ${state.count}`);
});
 
state.count++; // logs: Count is: 1
state.count++; // logs: Count is: 2
state.name = 'Sam'; // no log — not tracked by the effect

Validation Proxy

function validated(schema) {
  return new Proxy({}, {
    set(target, prop, value) {
      const validator = schema[prop];
      if (validator && !validator(value)) {
        throw new TypeError(`Invalid value for ${String(prop)}: ${value}`);
      }
      return Reflect.set(target, prop, value);
    },
    get(target, prop, receiver) {
      if (!(prop in target)) {
        throw new ReferenceError(`Property ${String(prop)} not defined`);
      }
      return Reflect.get(target, prop, receiver);
    },
  });
}
 
const user = validated({
  age: v => typeof v === 'number' && v >= 0 && v <= 150,
  email: v => typeof v === 'string' && v.includes('@'),
  name: v => typeof v === 'string' && v.length > 0,
});
 
user.name = 'Alex';     // OK
user.age = 30;           // OK
user.age = -1;           // TypeError: Invalid value for age: -1
user.email = 'invalid';  // TypeError: Invalid value for email: invalid

Immutable Proxy (Immer Pattern)

function createDraft(base) {
  const modified = new Set();
  const copy = { ...base };
 
  return new Proxy(copy, {
    get(target, prop) {
      return Reflect.get(target, prop);
    },
    set(target, prop, value) {
      modified.add(prop);
      return Reflect.set(target, prop, value);
    },
    deleteProperty(target, prop) {
      modified.add(prop);
      return Reflect.deleteProperty(target, prop);
    },
  });
}
 
function produce(base, recipe) {
  const draft = createDraft(base);
  recipe(draft);
  return Object.freeze({ ...draft });
}
 
const state = { users: ['Alex'], count: 1 };
const next = produce(state, draft => {
  draft.count = 2;
  draft.users = [...draft.users, 'Sam'];
});

Revocable Proxies

For access control — create a proxy that can be permanently disabled:

const { proxy, revoke } = Proxy.revocable(sensitiveData, handler);
 
proxy.secret;  // works
revoke();
proxy.secret;  // TypeError: Cannot perform 'get' on a proxy that has been revoked

Proxy Limitations

  • Performance — Proxy access is slower than direct property access (2-5x in microbenchmarks). Avoid on hot paths
  • Identity — proxy !== target. This can break === checks, Map/Set lookups, and WeakMap keys
  • Private fields — Proxy cannot intercept private class fields (#field), because the private slot exists on the target, not the proxy
  • Built-in exotic objects — Date, RegExp, Map, Set have internal slots that Proxy can't intercept. Wrapping them requires forwarding methods explicitly
const dateProxy = new Proxy(new Date(), {
  get(target, prop, receiver) {
    const value = Reflect.get(target, prop, receiver);
    return typeof value === 'function' ? value.bind(target) : value;
  },
});
dateProxy.getTime(); // works because we bind to target

Interview Signal

Senior candidates demonstrate:

  1. Trap mechanics — How traps map to language operations, the role of receiver in prototype chains
  2. Reflect discipline — Always forwarding through Reflect, not direct property access
  3. Reactive systems — Can sketch Vue 3 / MobX reactivity using Proxy + dependency tracking
  4. Limitations awareness — Performance cost, identity issues, private fields, exotic objects
  5. Real-world applications — Validation, immutability (Immer), revocable access, API mocking