Proxy Objects: Interception and Traps in JavaScript
Proxy objects intercept and customize operations on objects. This article covers traps, handlers, and practical metaprogramming patterns.
Introduction
Proxy objects enable:
- Intercepting property access
- Validating assignments
- Logging operations
- Lazy loading
- Virtual properties
- Advanced abstractions
Understanding Proxy helps you:
- Build frameworks
- Implement validation
- Create reactive systems
- Intercept operations
Proxy Basics
Creating a Proxy
// โ
Good: Create a basic proxy
const target = { x: 1, y: 2 };
const handler = {
get(target, prop) {
console.log(`Getting ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.x); // Logs: Getting x โ 1
proxy.y = 3; // Logs: Setting y to 3
Proxy Traps
// Common proxy traps:
const handler = {
get(target, prop) { }, // Property access
set(target, prop, value) { }, // Property assignment
has(target, prop) { }, // in operator
deleteProperty(target, prop) { }, // delete operator
ownKeys(target) { }, // Object.keys()
getOwnPropertyDescriptor(target, prop) { }, // Property descriptor
defineProperty(target, prop, descriptor) { }, // Object.defineProperty()
getPrototypeOf(target) { }, // Object.getPrototypeOf()
setPrototypeOf(target, proto) { }, // Object.setPrototypeOf()
isExtensible(target) { }, // Object.isExtensible()
preventExtensions(target) { }, // Object.preventExtensions()
apply(target, thisArg, args) { }, // Function call
construct(target, args) { } // new operator
};
Common Proxy Patterns
Validation
// โ
Good: Validate property assignments
const validator = {
set(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (prop === 'email' && !value.includes('@')) {
throw new TypeError('Invalid email');
}
target[prop] = value;
return true;
}
};
const user = new Proxy({}, validator);
user.age = 30; // OK
user.email = '[email protected]'; // OK
user.age = 'thirty'; // Error: Age must be a number
user.email = 'invalid'; // Error: Invalid email
Logging
// โ
Good: Log all operations
const logger = {
get(target, prop) {
console.log(`[GET] ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`[SET] ${prop} = ${value}`);
target[prop] = value;
return true;
},
deleteProperty(target, prop) {
console.log(`[DELETE] ${prop}`);
delete target[prop];
return true;
}
};
const obj = new Proxy({}, logger);
obj.x = 1; // [SET] x = 1
console.log(obj.x); // [GET] x
delete obj.x; // [DELETE] x
Lazy Loading
// โ
Good: Lazy load properties
const lazyLoader = {
get(target, prop) {
if (!(prop in target)) {
console.log(`Loading ${prop}...`);
// Simulate loading
target[prop] = `Loaded: ${prop}`;
}
return target[prop];
}
};
const data = new Proxy({}, lazyLoader);
console.log(data.user); // Loading user... โ Loaded: user
console.log(data.user); // Loaded: user (no loading)
Default Values
// โ
Good: Provide default values
const defaults = {
get(target, prop) {
return target[prop] ?? `Default: ${prop}`;
}
};
const config = new Proxy({}, defaults);
console.log(config.host); // Default: host
console.log(config.port); // Default: port
config.host = 'localhost';
console.log(config.host); // localhost
Advanced Proxy Patterns
Negative Array Indexing
// โ
Good: Support negative array indexing
const negativeArray = {
get(target, prop) {
const index = Number(prop);
if (index < 0) {
return target[target.length + index];
}
return target[index];
}
};
const arr = new Proxy([1, 2, 3, 4, 5], negativeArray);
console.log(arr[-1]); // 5 (last element)
console.log(arr[-2]); // 4 (second to last)
console.log(arr[0]); // 1 (first element)
Observable Objects
// โ
Good: Create observable objects
function observable(target, callback) {
return new Proxy(target, {
set(target, prop, value) {
if (target[prop] !== value) {
callback({ prop, oldValue: target[prop], newValue: value });
target[prop] = value;
}
return true;
}
});
}
// Usage
const user = observable({}, (change) => {
console.log(`${change.prop} changed from ${change.oldValue} to ${change.newValue}`);
});
user.name = 'John'; // name changed from undefined to John
user.age = 30; // age changed from undefined to 30
user.age = 31; // age changed from 30 to 31
Reactive Objects
// โ
Good: Create reactive objects
class Reactive {
constructor(data) {
this.data = data;
this.watchers = new Map();
return new Proxy(data, {
set: (target, prop, value) => {
if (target[prop] !== value) {
target[prop] = value;
this.notify(prop, value);
}
return true;
}
});
}
watch(prop, callback) {
if (!this.watchers.has(prop)) {
this.watchers.set(prop, []);
}
this.watchers.get(prop).push(callback);
}
notify(prop, value) {
if (this.watchers.has(prop)) {
this.watchers.get(prop).forEach(callback => {
callback(value);
});
}
}
}
// Usage
const user = new Reactive({ name: 'John', age: 30 });
user.watch('name', (newValue) => {
console.log(`Name changed to ${newValue}`);
});
user.name = 'Jane'; // Name changed to Jane
Function Interception
// โ
Good: Intercept function calls
const functionProxy = {
apply(target, thisArg, args) {
console.log(`Calling ${target.name} with args:`, args);
const result = target.apply(thisArg, args);
console.log(`Result:`, result);
return result;
}
};
function add(a, b) {
return a + b;
}
const proxiedAdd = new Proxy(add, functionProxy);
proxiedAdd(2, 3);
// Calling add with args: [2, 3]
// Result: 5
Constructor Interception
// โ
Good: Intercept constructor calls
const constructorProxy = {
construct(target, args) {
console.log(`Creating instance of ${target.name} with args:`, args);
return Reflect.construct(target, args);
}
};
class User {
constructor(name) {
this.name = name;
}
}
const ProxiedUser = new Proxy(User, constructorProxy);
const user = new ProxiedUser('John');
// Creating instance of User with args: ['John']
Practical Applications
API Client
// โ
Good: Create API client with proxy
function createAPIClient(baseURL) {
return new Proxy({}, {
get(target, prop) {
return async (...args) => {
const url = `${baseURL}/${prop}`;
const response = await fetch(url);
return response.json();
};
}
});
}
// Usage
const api = createAPIClient('https://api.example.com');
api.users(); // Fetches https://api.example.com/users
api.posts(); // Fetches https://api.example.com/posts
Form Validation
// โ
Good: Form validation with proxy
function createForm(schema) {
const data = {};
return new Proxy(data, {
set(target, prop, value) {
const validator = schema[prop];
if (validator && !validator(value)) {
throw new Error(`Invalid value for ${prop}`);
}
target[prop] = value;
return true;
}
});
}
// Usage
const form = createForm({
email: (value) => value.includes('@'),
age: (value) => value >= 18,
password: (value) => value.length >= 8
});
form.email = '[email protected]'; // OK
form.age = 25; // OK
form.password = 'secure123'; // OK
form.email = 'invalid'; // Error: Invalid value for email
Caching Proxy
// โ
Good: Cache function results
function createCachedFunction(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('From cache:', key);
return cache.get(key);
}
console.log('Computing:', key);
const result = target.apply(thisArg, args);
cache.set(key, result);
return result;
}
});
}
// Usage
const fibonacci = createCachedFunction((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
fibonacci(10); // Computes with caching
fibonacci(10); // From cache
Best Practices
-
Keep handlers simple:
// โ Good const handler = { get(target, prop) { return target[prop]; } }; -
Return true from set trap:
// โ Good set(target, prop, value) { target[prop] = value; return true; } -
Use Reflect for consistency:
// โ Good get(target, prop) { return Reflect.get(target, prop); } -
Handle errors properly:
// โ Good set(target, prop, value) { if (!isValid(value)) { throw new Error('Invalid value'); } target[prop] = value; return true; }
Common Mistakes
-
Forgetting to return true from set:
// โ Bad set(target, prop, value) { target[prop] = value; } // โ Good set(target, prop, value) { target[prop] = value; return true; } -
Not handling all traps:
// โ Bad - incomplete handler const handler = { get() { } }; // โ Good - handle necessary traps const handler = { get() { }, set() { } }; -
Performance issues:
// โ Bad - expensive operations in trap get(target, prop) { expensiveOperation(); return target[prop]; } // โ Good - optimize trap performance get(target, prop) { return target[prop]; }
Summary
Proxy objects are powerful for metaprogramming. Key takeaways:
- Intercept operations with traps
- Validate assignments
- Log operations
- Implement lazy loading
- Create reactive systems
- Build advanced abstractions
- Use Reflect for consistency
Related Resources
Next Steps
- Learn about Symbol: Unique Identifiers
- Explore Dynamic Code Evaluation
- Study Reflect API
- Practice proxy patterns
- Build advanced abstractions
Comments