How JavaScript Proxies Intercept Object Operations
JavaScript Proxies provide a powerful mechanism to wrap target
objects and customize their default behavior. Introduced in ECMAScript 6
(ES2015), a Proxy intercepts fundamental internal operations—such as
property lookup, assignment, enumeration, and function
invocation—allowing developers to redefine how these operations execute.
This article explains the mechanics of JavaScript Proxies, how handlers
and traps work, the role of the Reflect API, and practical
implementation patterns for intercepting object interactions.
Understanding the Core Components: Target, Handler, and Traps
A Proxy is instantiated using the Proxy constructor,
which requires two parameters:
const proxy = new Proxy(target, handler);- Target: The original object (an object, array, function, or another proxy) that the Proxy virtualizes.
- Handler: An object containing methods (called traps) that define the custom behavior when an operation is performed on the proxy.
- Traps: Methods within the handler that correspond to internal ECMAScript engine operations. If a trap is defined, the proxy executes that trap instead of the standard operation. If no trap is defined, the operation falls back to the default behavior on the target.
How Interception Works: Mapping Traps to Internal Methods
JavaScript engines interact with objects via low-level internal
methods (e.g., [[Get]], [[Set]],
[[Delete]], [[Call]]). A Proxy provides public
trap functions that directly intercept these internal methods.
Here are the most common internal methods and their corresponding Proxy traps:
| Internal Method | Proxy Trap | Triggering Operation |
|---|---|---|
[[Get]] |
get(target, prop, receiver) |
Reading a property (obj.prop
or obj['prop']) |
[[Set]] |
set(target, prop, value, receiver) |
Writing a property
(obj.prop = value) |
[[HasProperty]] |
has(target, prop) |
The in operator
('prop' in obj) |
[[Delete]] |
deleteProperty(target, prop) |
The delete operator
(delete obj.prop) |
[[Call]] |
apply(target, thisArg, argumentsList) |
Function call
(fn(...args)) |
[[Construct]] |
construct(target, argumentsList, newTarget) |
The new operator
(new Cls()) |
The Role of the
Reflect API
When redefining operations within a trap, you often still need to
perform the default action. The global Reflect object
provides methods that match the exact signatures and behaviors of the
Proxy traps.
Using Reflect ensures that default operations maintain
their original semantics, correctly forward the receiver
(preserving the expected this context), and return
appropriate boolean values for success or failure.
const user = { name: "Alex" };
const handler = {
get(target, prop, receiver) {
console.log(`Accessing property: ${String(prop)}`);
return Reflect.get(target, prop, receiver);
}
};
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name);
// Logs: "Accessing property: name"
// Output: "Alex"Common Interception Patterns
1. Data Validation with the
set Trap
The set trap intercepts property writes, allowing you to
enforce type safety or value constraints before modifying the target
object.
const validator = {
set(target, prop, value, receiver) {
if (prop === "age") {
if (typeof value !== "number" || value <= 0) {
throw new TypeError("Age must be a positive number.");
}
}
return Reflect.set(target, prop, value, receiver);
}
};
const profile = new Proxy({}, validator);
profile.age = 25; // Succeeds
// profile.age = -5; // Throws TypeError: Age must be a positive number.2. Providing Default
Values with the get Trap
The get trap can redefine how missing properties are
handled, avoiding undefined errors.
const dictionary = { hello: "bonjour" };
const safeDictionary = new Proxy(dictionary, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
}
return `Translation for '${String(prop)}' not found.`;
}
});
console.log(safeDictionary.hello); // "bonjour"
console.log(safeDictionary.goodbye); // "Translation for 'goodbye' not found."3. Intercepting
Function Calls with the apply Trap
When wrapping functions, the apply trap intercepts
execution, enabling logging, performance profiling, or argument
transformation.
function sum(a, b) {
return a + b;
}
const tracedSum = new Proxy(sum, {
apply(target, thisArg, argumentsList) {
console.log(`Called with arguments: ${argumentsList.join(", ")}`);
return Reflect.apply(target, thisArg, argumentsList);
}
});
console.log(tracedSum(5, 10));
// Logs: "Called with arguments: 5, 10"
// Output: 15Practical Applications
- Reactivity Systems: Modern frameworks (such as Vue 3) use proxies to track property reads and writes, triggering UI updates when data changes.
- Access Control & Security: Properties prefixed
with an underscore or symbol can be hidden or made read-only by throwing
errors in
setordeletePropertytraps. - Negative Array Indexing: Proxies can intercept
numeric properties on arrays to support negative indices (e.g.,
arr[-1]returning the last element). - API Mocking and Dynamic Objects: A proxy can dynamically construct nested API paths or SQL queries using chained property lookups.