🎭 JavaScript Proxies provide a powerful way to customize operations performed on objects. Let's explore what they are, how they work, and when to use them effectively.
Understanding Proxies
A Proxy wraps an object and can intercept operations like property lookup, assignment, and enumeration:
const target = {
name: 'John',
age: 30
};
const handler = {
get(target, property) {
console.log(`Accessing property: ${property}`);
return target[property];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: Accessing property: name
// Output: John
Basic Proxy Operations
Get and Set Traps
const user = {
firstName: 'John',
lastName: 'Doe'
};
const userProxy = new Proxy(user, {
get(target, property) {
if (property === 'fullName') {
return `${target.firstName} ${target.lastName}`;
}
return target[property];
},
set(target, property, value) {
if (property === 'age' && value < 0) {
throw new Error('Age cannot be negative');
}
target[property] = value;
return true;
}
});
console.log(userProxy.fullName); // "John Doe"
userProxy.age = 25; // OK
userProxy.age = -1; // Throws Error
Practical Applications
1. Validation
function createValidator(target, validations) {
return new Proxy(target, {
set(target, property, value) {
if (validations.hasOwnProperty(property)) {
const validator = validations[property];
if (!validator(value)) {
throw new Error(`Invalid value for ${property}`);
}
}
target[property] = value;
return true;
}
});
}
const user = {};
const validatedUser = createValidator(user, {
age: value => Number.isInteger(value) && value >= 0 && value <= 120,
email: value => /^[^@]+@[^@]+\.[^@]+$/.test(value)
});
validatedUser.age = 25; // OK
validatedUser.email = 'john@example.com'; // OK
validatedUser.age = -1; // Throws Error
validatedUser.email = 'invalid'; // Throws Error
2. Logging and Monitoring
function createLogger(target) {
return new Proxy(target, {
get(target, property) {
console.log(`Accessed: ${property}`);
return target[property];
},
set(target, property, value) {
console.log(`Set ${property} = ${value}`);
target[property] = value;
return true;
},
deleteProperty(target, property) {
console.log(`Deleted: ${property}`);
delete target[property];
return true;
}
});
}
const user = createLogger({});
user.name = 'John'; // Logs: Set name = John
console.log(user.name); // Logs: Accessed: name
delete user.name; // Logs: Deleted: name
3. Default Values
function createWithDefaults(target, defaults) {
return new Proxy(target, {
get(target, property) {
return property in target ?
target[property] :
defaults[property];
}
});
}
const defaults = {
theme: 'light',
language: 'en',
currency: 'USD'
};
const settings = createWithDefaults({}, defaults);
console.log(settings.theme); // "light"
settings.theme = 'dark';
console.log(settings.theme); // "dark"
4. Private Properties
function createPrivate(target, privateProps) {
return new Proxy(target, {
get(target, property) {
if (privateProps.includes(property)) {
throw new Error(`Cannot access private property: ${property}`);
}
return target[property];
},
set(target, property, value) {
if (privateProps.includes(property)) {
throw new Error(`Cannot modify private property: ${property}`);
}
target[property] = value;
return true;
}
});
}
const user = createPrivate(
{ name: 'John', _password: '123456' },
['_password']
);
console.log(user.name); // "John"
console.log(user._password); // Throws Error
5. Computed Properties
function createComputed(target, computedProps) {
return new Proxy(target, {
get(target, property) {
if (property in computedProps) {
return computedProps[property].call(target);
}
return target[property];
}
});
}
const user = createComputed(
{
firstName: 'John',
lastName: 'Doe',
age: 30
},
{
fullName() {
return `${this.firstName} ${this.lastName}`;
},
isAdult() {
return this.age >= 18;
}
}
);
console.log(user.fullName); // "John Doe"
console.log(user.isAdult); // true
Advanced Use Cases
1. API Response Caching
function createCachingProxy(target, ttl = 5000) {
const cache = new Map();
return new Proxy(target, {
async apply(target, thisArg, args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const result = await target.apply(thisArg, args);
cache.set(key, {
data: result,
timestamp: Date.now()
});
return result;
}
});
}
const fetchData = async (url) => {
const response = await fetch(url);
return response.json();
};
const cachedFetch = createCachingProxy(fetchData);
2. Virtual DOM Properties
function createVirtualDOM(element) {
return new Proxy({}, {
get(target, property) {
if (property === 'innerHTML') {
return element.innerHTML;
}
return element.getAttribute(property);
},
set(target, property, value) {
if (property === 'innerHTML') {
element.innerHTML = value;
} else {
element.setAttribute(property, value);
}
return true;
}
});
}
const div = document.createElement('div');
const virtualDiv = createVirtualDOM(div);
virtualDiv.innerHTML = 'Hello World';
virtualDiv.class = 'greeting';
3. Reactive Data Binding
function createReactive(target) {
const subscribers = new Map();
return new Proxy(target, {
get(target, property) {
return target[property];
},
set(target, property, value) {
const oldValue = target[property];
target[property] = value;
if (subscribers.has(property)) {
subscribers.get(property).forEach(callback => {
callback(value, oldValue);
});
}
return true;
}
});
}
const state = createReactive({
count: 0,
message: ''
});
// Subscribe to changes
function subscribe(property, callback) {
if (!subscribers.has(property)) {
subscribers.set(property, new Set());
}
subscribers.get(property).add(callback);
}
subscribe('count', (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
Best Practices
- Use Proxies for Cross-Cutting Concerns
- Consider Performance Implications
- Handle Edge Cases
- Document Proxy Behavior
// Good Practice Example
function createProxy(target, options = {}) {
const {
validateAccess = true,
logging = false,
errorHandler = console.error
} = options;
return new Proxy(target, {
get(target, property) {
try {
if (validateAccess && !(property in target)) {
throw new Error(`Property ${property} does not exist`);
}
if (logging) {
console.log(`Accessing ${property}`);
}
return target[property];
} catch (error) {
errorHandler(error);
return undefined;
}
}
});
}
Conclusion
JavaScript Proxies are powerful tools for metaprogramming and implementing advanced programming patterns. They're particularly useful for validation, logging, access control, and creating reactive systems. While powerful, use them judiciously and consider their performance implications in performance-critical applications.