Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ES2015+ ~22 min· topic 6 of 9

Topic 14.6

Proxy and Reflect

In one line

A Proxy wraps an object and lets you intercept basic operations on it (reading, writing, deleting, in, calling) with handler functions called traps; Reflect offers the default version of each operation, so a trap can add behaviour and then do the normal thing.

Think of it like this

A receptionist in front of a manager's office. Visitors don't walk straight in: they talk to the receptionist, who can log the visit, check an ID, turn someone away, or pass the message on unchanged. A Proxy is that receptionist for an object. The manager is the target object, and the receptionist's rule book is the handler.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Proxy
An object that stands in front of another object and can intercept what you do to it.
Target
The real object a proxy wraps.
Handler
The object holding the trap functions for a proxy.
Trap
A handler function that runs instead of a basic operation, like get for reading a property.
Reflect
A built-in object with functions that do each basic object operation the normal way.
Receiver
The object an operation started on (usually the proxy), passed to get and set traps so getters and setters see the right this.
Invariant
A rule a trap must not break, such as reporting a frozen property's real value.
Reactivity
Updating the screen automatically when data changes, by noticing reads and writes.

Step by step

01Wrap an object with an empty handler

With no traps, every operation passes straight through. The proxy looks and acts like the target, but it's a separate object.

plain.jswhole filejavascript
const target = { name: "Asha" };
const p = new Proxy(target, {});
p.age = 12;
console.log(target.age); // 12: the write reached the target
console.log(p === target); // false

02Add a get trap

A trap replaces the operation. Use Reflect inside it to carry out the default and add your own behaviour around it.

Add a get trapdiagram
Rendering diagram…

03Validate writes in a set trap

Check the value. If it's bad, throw a helpful error. If it's fine, store it with Reflect.set and return its result (true).

validate.jswhole filejavascript
const person = new Proxy({}, {
  set(target, key, value, receiver) {
    if (key === "age" && !Number.isInteger(value)) {
      throw new TypeError("age must be an integer");
    }
    return Reflect.set(target, key, value, receiver);
  },
});
person.age = 30;    // fine
person.age = "old"; // TypeError: age must be an integer

04Trap function calls

For a function target, apply runs on every call. Here it measures how often a function is called without changing it.

count-calls.jswhole filejavascript
let calls = 0;
const counted = new Proxy(Math.max, {
  apply(target, thisArg, args) {
    calls++;
    return Reflect.apply(target, thisArg, args);
  },
});
counted(1, 5);
counted(2, 3);
console.log(calls); // 2

05Make it reactive

Call a function whenever a property changes. Frameworks extend this by recording, in the get trap, which render functions read which properties, so only those re-run.

06Revoke access

Proxy.revocable(target, handler) returns { proxy, revoke }. After revoke(), any operation on the proxy throws a TypeError. Handy for handing out temporary access to an object.

Try it yourself

  1. 1

    Make it read-only

    In the first example, change the set trap to log blocked and return true without calling Reflect.set. Predict what target looks like at the end.

  2. 2

    Validate a name

    In the second example, add a name rule: a string of at least 2 characters. Predict which of user.name = "A" and user.name = "Asha" throws.

  3. 3

    Add a second display

    In the reactive example, add a <p>Squared: <strong id="square"></strong></p> and update it in render. Click +1 and watch all three update from one write.

Code & diagrams

Watching every operation ES2015+ New tab

Every change went through to `target`, but you saw it happen. Logging the proxy itself would fire `get` many times, so we log the target.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

get name
set age = 13
has age
true
delete age
Asha { name: 'Asha' }
false
Validation, defaults and negative indexes ES2015+ New tab

Property keys always reach traps as strings (or symbols), so `list[-1]` arrives as the string `"-1"`. Checking the type first matters: `Number(symbol)` would throw when the array is spread.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Invalid value for age: -5
30
3 0
30 10 10 3
The idea behind reactive frameworks ES2015+ New tab

No code calls `render()` after changing the count: the proxy notices writes. Setting the same value twice doesn't re-render. Click +1 to see more lines.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

count: 0 -> 5
count: 5 -> 6

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Return false from a set trap in strict code

In a module or after "use strict", use set() { return false; } and then write p.age = 1.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'age'

Break #2

Read through the proxy inside its own get trap

Write get(t, key, receiver) { return receiver[key]; }.

terminal
$ Console
── what you'll see ──
Uncaught RangeError: Maximum call stack size exceeded

Break #3

Call a method that uses #private fields

Wrap an instance of class C { #x = 1; getX() { return this.#x; } } in new Proxy(obj, {}) and call proxy.getX().

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read private member #x from an object whose class did not declare it

Myth vs fact

Myth

A Proxy copies the target.

Fact

It wraps it. Writes that pass through change the target, and changes to the target are visible through the proxy.

Myth

Proxies are too slow to use anywhere.

Fact

Each trapped operation costs more than a plain property access, and engines can't optimise proxies as well. That matters in hot loops, not for state objects that change on clicks. Vue 3 uses proxies for all reactive state.

Myth

Reflect is a different way to do reflection like in Java.

Fact

It's simply the set of default object operations, as functions, matching the 13 traps one to one.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Proxies trap only the 13 internal methods. They can't intercept === (identity), typeof, or access to internal slots: new Proxy(new Map(), {}).get("a") throws TypeError: Method Map.prototype.get called on incompatible receiver #<Map>, because Map methods need the real Map's internal data. Bind such methods to the target in the get trap.

  • ▸

    Always pass receiver to Reflect.get/Reflect.set. If the target has a getter or setter, receiver decides what this is inside it. Dropping it breaks getters that read other properties, and breaks proxies used as prototypes.

  • ▸

    Proxy.revocable lets you build membranes: wrap an object graph so every object handed out is also proxied, then cut all access with one revoke(). Secure sandboxes (and Agoric's SES) use this pattern.

  • ▸

    Proxies are not observable as such: there's no isProxy() in the language, Array.isArray(new Proxy([], {})) is true, and typeof on a function proxy is "function". DevTools does show Proxy(Object) and the handler.

Remember this

  1. 1

    new Proxy(target, handler) creates a proxy: a new object that stands in for target. Every basic operation on the proxy first looks for a matching function in handler, called a trap. If there's a trap, it runs; if not, the operation goes straight to the target. An empty handler {} gives a proxy that behaves exactly like the target.

  2. 2

    There are 13 traps, one for each internal operation the spec defines on objects: get (reading p.x), set (writing p.x = 1), has ("x" in p), deleteProperty (delete p.x), ownKeys (Object.keys, for...in, Reflect.ownKeys), getOwnPropertyDescriptor, defineProperty, getPrototypeOf, setPrototypeOf, isExtensible, preventExtensions, and for function targets apply (calling p()) and construct (new p()). Each trap receives the target, the property key and, for get/set, the receiver (the object the operation started on, usually the proxy).

  3. 3

    Reflect is a built-in object with one function for each trap, with the same name and arguments: Reflect.get(target, key, receiver), Reflect.set(...), Reflect.has(...), Reflect.ownKeys(...) and so on. Each does the default behaviour. So the usual pattern is: do your extra work, then return Reflect.set(target, key, value, receiver). Reflect methods also return true/false instead of throwing where the old Object methods threw (Reflect.defineProperty vs Object.defineProperty), and Reflect.ownKeys lists string and symbol keys.

  4. 4

    A set trap must return true when the write succeeded. If it returns false, sloppy-mode code ignores the failure silently, but strict-mode code (modules, classes, "use strict", see Topic 14.9) throws TypeError: 'set' on proxy: trap returned falsish for property 'age'. For validation, either throw your own clear error from the trap or return false and rely on strict mode.

  5. 5

    Traps must respect invariants: rules that keep the language consistent. For example, if the target has a non-configurable, non-writable property, the get trap must report its real value; ownKeys must include every non-configurable key. Breaking an invariant throws a TypeError. A proxy is also a different object from its target (proxy !== target), and methods that use #private fields fail when called on a proxy, because the proxy doesn't have the target's private slots.

  6. 6

    Uses: validation (reject bad values as they're written), defaults (return 0 for missing keys), logging and debugging, negative array indexes, read-only views, revocable access (Proxy.revocable gives a proxy you can switch off), and reactivity. Vue 3's reactivity system wraps your state in proxies: the get trap records which component read which property, and the set trap re-renders exactly those components. That's the idea you'll build in the third example.

Explain it without notes

01

What is a Proxy and what is a trap? Give three traps and the operations they intercept.

02

Why is Reflect usually used inside traps?

03

How would you validate property values with a Proxy, and how does strict mode affect it?

04

Explain how a Proxy can make UI reactive, and what its limits are.

Practice

01

Write readOnly(obj) that returns a proxy where reads work but any write or delete throws Error("read-only"). Show both behaviours.

02

Write a proxy env where reading a missing key throws ReferenceError("Unknown setting: <key>") instead of returning undefined, so typos are caught.

Trade-offs

  • ↔

    Proxies add powerful behaviour without changing the code that uses the object, but that "magic" makes code harder to read and debug, and every trapped operation is slower than a plain one.

  • ↔

    Validation in a proxy catches every write in one place; explicit setter methods or a schema check at the boundary are more visible and work with TypeScript types more naturally.

Done when you can

  • I can create a Proxy with get, set, has and deleteProperty traps.

  • I can use Reflect to carry out the default behaviour, passing the receiver.

  • I can write a validating proxy and know how strict mode treats false from set.

  • I can explain the reactivity idea behind Vue-style frameworks.

  • I know proxy limits: identity, private fields, internal slots and invariants.