Discover how to use JavaScript's metaprogramming features to write more powerful and flexible code using Proxies and the Reflect API. 🔮
Understanding Metaprogramming 🎯
Metaprogramming allows programs to treat code as data, enabling dynamic behavior modification at runtime. JavaScript provides powerful metaprogramming capabilities through Proxies and the Reflect API.
Proxies Fundamentals
Basic Proxy Creation
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
Common Trap Types
const handler = {
// Intercept property access
get(target, property) {},
// Intercept property assignment
set(target, property, value) {},
// Intercept property deletion
deleteProperty(target, property) {},
// Intercept property enumeration
ownKeys(target) {},
// Intercept property existence checks
has(target, property) {}
};
Real-World Proxy Use Cases 🌟
1. Validation
function createValidator(target, validations) {
return new Proxy(target, {
set(target, property, value) {
if (validations[property]) {
const valid = validations[property](value);
if (!valid) {
throw new Error(`Invalid value for ${property}`);
}
}
target[property] = value;
return true;
}
});
}
const user = createValidator({}, {
age: value => typeof value === 'number' && value >= 0,
email: value => /^[^@]+@[^@]+\.[^@]+$/.test(value)
});
user.age = 25; // Works
user.age = -5; // 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;
}
});
}
const user = createLogger({
name: 'John'
});
user.name; // Logs: Accessed: name
user.name = 'Jane'; // Logs: Set name = Jane
3. Default Values
function withDefaults(target, defaults) {
return new Proxy(target, {
get(target, property) {
return property in target ?
target[property] :
defaults[property];
}
});
}
const config = withDefaults({
theme: 'dark'
}, {
theme: 'light',
fontSize: 16,
language: 'en'
});
console.log(config.theme); // 'dark'
console.log(config.fontSize); // 16
The Reflect API 🔄
Basic Reflect Operations
// Object property manipulation
Reflect.get(target, propertyKey[, receiver])
Reflect.set(target, propertyKey, value[, receiver])
Reflect.has(target, propertyKey)
Reflect.deleteProperty(target, propertyKey)
Reflect.defineProperty(target, propertyKey, attributes)
// Function manipulation
Reflect.apply(target, thisArgument, argumentsList)
Reflect.construct(target, argumentsList[, newTarget])
// Prototype manipulation
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
Reflect with Proxies
const handler = {
get(target, property, receiver) {
console.log(`Accessing: ${property}`);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`Setting: ${property} = ${value}`);
return Reflect.set(target, property, value, receiver);
}
};
const proxy = new Proxy({}, handler);
Advanced Patterns 🚀
1. Virtual Objects
function createVirtualObject(handler) {
return new Proxy({}, {
get(target, property) {
return handler.get ? handler.get(property) : undefined;
},
set(target, property, value) {
if (handler.set) {
handler.set(property, value);
}
return true;
}
});
}
const storage = createVirtualObject({
get(key) {
return localStorage.getItem(key);
},
set(key, value) {
localStorage.setItem(key, value);
}
});
2. Method Decorators
function trace(target, method) {
return new Proxy(target[method], {
apply(target, thisArg, args) {
console.log(`Calling ${method} with:`, args);
const result = target.apply(thisArg, args);
console.log(`${method} returned:`, result);
return result;
}
});
}
class Calculator {
constructor() {
this.add = trace(this, 'add');
}
add(a, b) {
return a + b;
}
}
3. Property Access Control
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;
}
});
}
Performance Considerations 📊
// Direct property access
const obj = { value: 42 };
console.time('direct');
for (let i = 0; i < 1000000; i++) {
obj.value;
}
console.timeEnd('direct');
// Proxy property access
const proxy = new Proxy(obj, {
get: (target, prop) => target[prop]
});
console.time('proxy');
for (let i = 0; i < 1000000; i++) {
proxy.value;
}
console.timeEnd('proxy');
Best Practices 📝
- Use Proxies judiciously
- Implement proper error handling
- Consider performance implications
- Keep handlers focused and simple
- Use Reflect API for consistency
- Document proxy behavior
- Test edge cases thoroughly
- Consider memory usage
- Implement proper cleanup
- Use TypeScript for better type safety
Debugging Tips 🐛
function debugProxy(target) {
return new Proxy(target, {
get(target, property) {
debugger;
return target[property];
},
set(target, property, value) {
debugger;
target[property] = value;
return true;
}
});
}
Additional Resources
JavaScript metaprogramming with Proxies and Reflect provides powerful tools for creating flexible and maintainable code. While these features should be used judiciously, they can solve complex problems elegantly when applied appropriately.