Kodokon kodokon.com

Proxy, Reflect, and metaprogramming

Intercept the language's fundamental operations with Proxy traps and cleanly delegate default behavior with Reflect.

10 min · 3 questions

Open this lesson in Kodokon

A Proxy wraps a target object and intercepts the language's internal operations through thirteen traps: get, set, has (the in operator), deleteProperty, ownKeys, apply, construct, and so on. The Reflect object exposes exactly the same operations as functions, with the same signatures as the traps. It is the foundation of Vue 3's reactivity, of dynamic mocks, and of ORM-style APIs.

JAVASCRIPT
const target = { name: "Ada" };

const traced = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log("get " + String(prop));
    return Reflect.get(obj, prop, receiver);
  }
});

traced.name; // get name
A minimal get trap delegated to Reflect.

Why Reflect.get(obj, prop, receiver) rather than obj[prop]? Because of the receiver. If the property is an accessor (a getter), Reflect.get invokes it with this pointing at the receiver - the proxy or an object inheriting from it - whereas obj[prop] would set this to the raw target. Without the receiver, getters that read this break as soon as an object inherits from the proxy.

JAVASCRIPT
const base = {
  get label() {
    return this.name;
  }
};

const proxy = new Proxy(base, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
});

const child = Object.create(proxy);
child.name = "Ada";
console.log(child.label); // "Ada" thanks to the receiver
The receiver preserves this in inherited getters.

The serious use cases: validation on write, virtual properties computed on the fly, logging, access control (hiding keys prefixed with _ via has and ownKeys), or objects with default behavior. Note that Reflect.set returns a boolean instead of throwing in strict mode: your trap must propagate that return value.

JAVASCRIPT
function validated(target) {
  return new Proxy(target, {
    set(obj, prop, value, receiver) {
      if (prop === "age" && !Number.isInteger(value)) {
        throw new TypeError("age must be an integer");
      }
      return Reflect.set(obj, prop, value, receiver);
    }
  });
}

const user = validated({ age: 30 });
user.age = 31;       // ok
// user.age = "old"; // TypeError
Transparent validation on write.

Knowledge check

Make sure you remember the key points of this lesson.

  1. In a get trap, why prefer Reflect.get(target, prop, receiver) over target[prop]?
    • It is faster because Reflect bypasses the prototype
    • So that getters run with the right this (the receiver) rather than the raw target
    • Because target[prop] would re-trigger the trap and cause infinite recursion
    • The two forms are strictly equivalent
  2. Which trap intercepts the expression "id" in obj?
    • get
    • has
    • ownKeys
    • getOwnPropertyDescriptor
  3. A get trap returns 42 for a target property whose value is 1, declared non-configurable and non-writable. What happens?
    • The proxy returns 42, the trap always takes priority
    • The engine silently returns 1
    • The engine throws a TypeError for an invariant violation
    • The behavior depends on the JavaScript engine