Understanding JavaScript Proxies: Objects and Arrays
In our previous article, we introduced JavaScript Proxies as a powerful way to intercept and customize object behaviors. Now, let's dive deeper into practical applications, focusing on how Proxies transform the way we work with objects and arrays.
Basic Proxy Handlers
The true power of Proxies lies in their handlers—objects containing "traps" that intercept different operations. The most common traps are get
and set
, which intercept property access and assignment respectively.
The get
Trap: Intercepting Property Access
The get
trap fires whenever a property is accessed. This enables powerful patterns like logging, computed properties, and default values:
const person = {
firstName: "Emma",
lastName: "Chen"
};
const personProxy = new Proxy(person, {
get(target, property, receiver) {
// Log all property access
console.log(`Accessing: ${property}`);
// Handle special properties
if (property === 'fullName') {
return `${target.firstName} ${target.lastName}`;
}
// Provide defaults for missing properties
if (!(property in target)) {
return `Property '${property}' doesn't exist`;
}
// Return the actual value
return target[property];
}
});
console.log(personProxy.firstName); // Logs "Accessing: firstName", returns "Emma"
console.log(personProxy.fullName); // Logs "Accessing: fullName", returns "Emma Chen"
console.log(personProxy.age); // Logs "Accessing: age", returns "Property 'age' doesn't exist"
This simple example demonstrates three powerful patterns:
- Logging property access — useful for debugging and tracking dependencies
- Computed properties — creating virtual properties that don't exist on the original object
- Default values — gracefully handling missing properties
The set
Trap: Validating and Transforming Values
The set
trap allows us to intercept property assignments, enabling validation, transformation, and side effects:
const user = {
name: "Sam",
email: "sam@example.com"
};
const userProxy = new Proxy(user, {
set(target, property, value, receiver) {
// Validate email format
if (property === 'email' && !/^\S+@\S+\.\S+$/.test(value)) {
throw new Error('Invalid email format');
}
// Transform data to standardized format
if (property === 'name') {
value = value.trim();
}
// Log changes
console.log(`Changed ${property} from ${target[property]} to ${value}`);
// Update the target
target[property] = value;
return true; // Indicate success
}
});
userProxy.name = " Taylor Kim "; // Trims to "Taylor Kim"
try {
userProxy.email = "invalid-email";
} catch (e) {
console.error(e.message); // "Invalid email format"
}
This pattern is invaluable for:
- Data validation — preventing invalid values from being stored
- Data normalization — ensuring consistent formats
- Change tracking — logging or reacting to state changes
Using Proxies with Arrays
JavaScript arrays are just objects with special behaviors, so Proxies work beautifully with them too. Let's explore some powerful array-specific patterns:
Tracking Array Mutations
Arrays have methods like push
, pop
, and splice
that modify the array. We can track these operations with Proxies:
function createTrackedArray(initialArray = []) {
return new Proxy(initialArray, {
get(target, property, receiver) {
// Intercept array methods
const value = target[property];
if (typeof value === 'function') {
return function(...args) {
console.log(`Array method ${property} called with args: ${args}`);
return value.apply(target, args);
};
}
return value;
},
set(target, property, value, receiver) {
console.log(`Setting index ${property} to ${value}`);
target[property] = value;
return true;
}
});
}
const trackedArray = createTrackedArray([1, 2, 3]);
trackedArray.push(4); // "Array method push called with args: 4"
trackedArray[1] = 10; // "Setting index 1 to 10"
trackedArray.pop(); // "Array method pop called with args: "
console.log(trackedArray); // [1, 10, 3]
This example intercepts both direct property assignments and method calls like push
and pop
.
Preventing Negative Indices
JavaScript allows negative array indices but treats them as regular properties rather than accessing elements from the end. We can change this behavior:
const smartArray = new Proxy([1, 2, 3, 4, 5], {
get(target, property, receiver) {
// Convert negative indices to positive ones
const index = Number(property);
if (Number.isInteger(index) && index < 0) {
const positiveIndex = target.length + index;
if (positiveIndex >= 0) {
return target[positiveIndex];
}
}
return target[property];
}
});
console.log(smartArray[0]); // 1
console.log(smartArray[-1]); // 5 (last element)
console.log(smartArray[-3]); // 3 (third from end)
This pattern is reminiscent of Python's array indexing, allowing negative indices to access elements from the end of the array.
Creating Reactive Arrays
For frontend frameworks, reactive arrays that trigger updates on changes are essential. Proxies enable this pattern elegantly:
function createReactiveArray(initialArray = [], onChange) {
return new Proxy(initialArray, {
set(target, property, value, receiver) {
const oldValue = target[property];
target[property] = value;
// Only trigger for array indices, not for properties like 'length'
const index = Number(property);
if (Number.isInteger(index) && index >= 0) {
onChange({
type: 'update',
index,
oldValue,
newValue: value
});
}
return true;
},
get(target, property, receiver) {
const value = target[property];
// Wrap array methods to detect changes
if (typeof value === 'function') {
return function(...args) {
const oldLength = target.length;
const result = value.apply(target, args);
// Detect what method was called
if (property === 'push') {
onChange({
type: 'push',
added: args,
newLength: target.length
});
} else if (property === 'pop') {
onChange({
type: 'pop',
removed: result,
newLength: target.length
});
} // Additional methods could be handled here
return result;
};
}
return value;
}
});
}
// Usage example
const todos = createReactiveArray(
['Learn JavaScript', 'Master Proxies'],
(change) => console.log('Array changed:', change)
);
todos.push('Build an app');
// Logs: "Array changed: {type: 'push', added: ['Build an app'], newLength: 3}"
todos[1] = 'Master JavaScript Proxies';
// Logs: "Array changed: {type: 'update', index: 1, oldValue: 'Master Proxies', newValue: 'Master JavaScript Proxies'}"
This pattern is fundamental to reactive UI frameworks like Vue.js, which use similar techniques to detect and respond to data changes.
Why This Matters in Real Development
These patterns go beyond academic exercises—they solve real problems:
- Validation — Ensuring data integrity without verbose checks throughout your code
- Debugging — Tracking property access and changes for troubleshooting
- Reactivity — Building UI components that update automatically on data changes
- API Design — Creating intuitive interfaces with custom behaviors
By leveraging Proxies, you can create more robust, maintainable code with fewer bugs and better developer experience.
Top comments (0)