Proxy and Reflect

JavaScript · Advanced · 15 min

Proxies power reactivity systems, validation layers, mocks and API wrappers.

A Proxy wraps a target object and lets you intercept fundamental operations on it through traps defined in a handler:

const p = new Proxy(target, {
  get(target, key, receiver) { ... },
  set(target, key, value, receiver) { ... },
});

There are traps for every internal operation: get, set, has (in), deleteProperty, ownKeys (Object.keys, for...in), getOwnPropertyDescriptor, defineProperty, getPrototypeOf, and for function targets apply (calls) and construct (new). Any trap you omit falls through to the target.

Reflect has a static method with the same name and signature for each trap. Use it to perform the default behaviour inside a trap: return Reflect.get(target, key, receiver). Passing receiver matters: it makes getters on the target (or its prototype chain) run with this set to the proxy, so accesses inside them are intercepted too. Reflect.set and friends also return booleans instead of throwing, which is exactly what traps must return.

Common uses:

  • Validation: reject bad writes in set.
  • Reactivity: record reads in get, trigger updates in set — Vue 3's reactivity is built this way.
  • Defaults / negative indexes / logging / mocks for tests.

Limits and costs:

  • The engine enforces invariants: e.g. a trap can't report a non-configurable property as missing; violations throw TypeError.
  • A proxy is a different object: proxy !== target.
  • Objects with internal slots break through proxies: Map methods and class #private fields fail when this is the proxy. Wrap the target's methods or avoid proxying such objects.
  • Every operation goes through a function call, so proxies are slower than plain objects in hot paths.

Example

function validated(target, schema) {
  return new Proxy(target, {
    set(obj, key, value, receiver) {
      const check = schema[key];
      if (check && !check(value)) throw new TypeError(`Invalid ${String(key)}: ${value}`);
      return Reflect.set(obj, key, value, receiver);
    },
    get(obj, key, receiver) {
      if (!(key in obj)) console.log(`(read of missing ${String(key)})`);
      return Reflect.get(obj, key, receiver);
    },
  });
}

const user = validated({ name: "Ada", age: 36 }, {
  age: v => Number.isInteger(v) && v >= 0,
});

user.age = 37;
try { user.age = -5; } catch (e) { console.log(e.message); } // Invalid age: -5
console.log(user.age);        // 37
user.email;                   // (read of missing email)

const m = new Proxy(new Map(), {});
try { m.set("k", 1); } catch (e) { console.log(e.constructor.name); } // TypeError

Remember

  • A Proxy intercepts operations via traps; missing traps forward to the target
  • Use Reflect.* inside traps, passing receiver, for correct default behaviour
  • Proxies break on internal slots (Map, #private) and cost performance

Watch out for

  • Returning nothing from a set trap: in strict mode a falsy result throws TypeError.

Check yourself

  1. Why pass receiver to Reflect.get(target, key, receiver) in a get trap?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming