JavaScript Proxy Table
| Trap | Fires when | Field note |
|---|---|---|
get(target, prop, receiver) | Any property READ | The validation/logging hook - receiver is the proxy or a prototype-derived object |
set(target, prop, value, receiver) | Any property WRITE | Return true to confirm; a falsy return fails the write (silent in sloppy code, TypeError in strict) |
has(target, prop) | The in operator | Hide internals: return false for _private keys - for...in is NOT trapped (ownKeys covers it) |
deleteProperty(target, prop) | The delete operator | Immutable layers: refuse deletes on frozen records and audit logs |
apply(target, thisArg, args) | Any function CALL | Wrap timing, memoization, access control - the AOP workhorse |
construct(target, args) | The new operator | Must return an object - returning a primitive throws TypeError |
ownKeys(target) | Object.keys / getOwnPropertyNames | Cannot omit non-configurable own keys - the invariant guard |
revocable(target, handler) | Factory, not a trap | Returns {proxy, revoke} - revoke() makes every later operation throw; the sandbox escape hatch |
A Proxy wraps an object and intercepts its FUNDAMENTAL operations - every read, write, in check, delete, function call and new expression passes through your handler's trap first. One handler layer stands in front of the whole object: you write a get trap once and every property read is validated, logged or defaulted.
Bottom line: the set trap is a runtime schema validator that never touches the target, the apply trap is the cleanest logging and memoization wrapper there is, and revocable() hands you capability revocation - the raw material of sandboxes. Vue 3's entire reactivity system is this table running in production.
The honest part: the engine enforces invariants on what traps may claim. Report a non-configurable property as missing, return a primitive from construct, and you get a TypeError instead of a silently broken object - meta-programming with guardrails, not a blank cheque.
How to use
- Create one: new Proxy(target, handler) - the handler object's methods ARE the traps; an empty handler behaves exactly like the target.
- Validate writes in set: check value against your schema, return true - a falsy return fails the assignment (throws TypeError in strict mode).
- Wrap functions with apply: measure timing, cache results or gate access - the target function never knows it was wrapped.
Frequently asked questions
What is the difference between a Proxy and the target object?
The target is the real object holding the data; the Proxy is the front door every operation walks through. The proxy holds a reference to the target and forwards what its traps do not intercept. Identity is preserved in one direction only: proxy === target is false, but operations the handler does not trap behave exactly as on the target. Code that stores the target directly bypasses every trap - keep the target private, hand out only the proxy, or validation can be skipped by anyone with the raw reference.
Why does my get trap break method calls?
The classic this-detach bug: a get trap that returns target.method hands back the function WITHOUT its receiver, so this is undefined (or global) when it runs. Fix it by binding or by Reflect.get(target, prop, receiver), which forwards the receiver so methods run against the proxy. This is also why frameworks combine Proxy with Reflect - Reflect's methods take the receiver argument that keeps this pointed at the proxy through every forwarded operation.
What are Proxy invariants and when do they throw?
Invariants are the engine's protection against traps that lie about the target's real state. ownKeys cannot omit non-configurable own keys; get cannot return undefined for a non-configurable, non-writable property; construct must return an object. Violate one and you get a TypeError at the operation site - the philosophy is that proxies may intercept semantics but may not falsify them, so existing code that relies on property configurability keeps working even under surveillance.
When should I use Proxy instead of getters, setters or decorators?
Use property accessors when you know the keys at author time and want per-property control - they are faster and simpler. Reach for Proxy when the keys are unknown or unbounded: validating every write to a form state bag, logging every call on an API client, revoking access to an object after a session ends. revocable() has no accessor equivalent - it is the only way to make an object permanently unusable from the outside, which is why sandboxed eval environments are built on it.