Proxy and Reflect API
The Proxy and Reflect APIs are powerful ES6 features that allow you to intercept and customize fundamental operations on objects. Proxies enable meta-programming, validation, logging, and much more. Let's explore how to use these advanced features.
What is a Proxy?
A Proxy wraps an object and intercepts operations performed on it. You can customize behavior for fundamental operations like property access, assignment, enumeration, function invocation, and more.
Key Concept: A Proxy acts as a middleman between you and the target object, allowing you to intercept and redefine operations.
Creating a Basic Proxy
Create a proxy with a target object and a handler containing traps:
// Target object
const target = {
name: "John",
age: 30
};
// Handler with traps
const handler = {
// Intercept property access (get trap)
get(target, property) {
console.log(`Getting property: ${property}`);
return target[property];
},
// Intercept property assignment (set trap)
set(target, property, value) {
console.log(`Setting property: ${property} = ${value}`);
target[property] = value;
return true; // Indicates success
}
};
// Create the proxy
const proxy = new Proxy(target, handler);
// Use the proxy
console.log(proxy.name); // Logs: "Getting property: name", then "John"
proxy.age = 31; // Logs: "Setting property: age = 31"
console.log(proxy.age); // Logs: "Getting property: age", then 31
Common Proxy Traps
Proxies support many traps for different operations:
Common Traps:
get(target, property, receiver):
- Intercepts property access
- Example: obj.prop or obj["prop"]
set(target, property, value, receiver):
- Intercepts property assignment
- Example: obj.prop = value
has(target, property):
- Intercepts the "in" operator
- Example: "prop" in obj
deleteProperty(target, property):
- Intercepts property deletion
- Example: delete obj.prop
apply(target, thisArg, argumentsList):
- Intercepts function calls
- Example: func(...args)
construct(target, argumentsList, newTarget):
- Intercepts "new" operator
- Example: new Func(...args)
Validation with Proxies
Use proxies to validate data before setting properties:
const validator = {
set(target, property, value) {
if (property === "age") {
if (typeof value !== "number") {
throw new TypeError("Age must be a number");
}
if (value < 0 || value > 150) {
throw new RangeError("Age must be between 0 and 150");
}
}
if (property === "email") {
if (!value.includes("@")) {
throw new Error("Invalid email format");
}
}
target[property] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.age = 30; // Works
console.log(person.age); // 30
person.email = "john@example.com"; // Works
console.log(person.email); // "john@example.com"
// These will throw errors:
// person.age = "thirty"; // TypeError: Age must be a number
// person.age = -5; // RangeError: Age must be between 0 and 150
// person.email = "invalid"; // Error: Invalid email format
Use Case: Proxies are excellent for runtime type checking and data validation without cluttering your code with manual checks.
Default Values with Proxies
Return default values for non-existent properties:
const withDefaults = (target, defaultValue) => {
return new Proxy(target, {
get(target, property) {
if (property in target) {
return target[property];
}
return defaultValue;
}
});
};
const config = withDefaults({
port: 3000,
host: "localhost"
}, "Not configured");
console.log(config.port); // 3000
console.log(config.host); // "localhost"
console.log(config.database); // "Not configured"
console.log(config.anything); // "Not configured"
Logging and Debugging with Proxies
Track all operations on an object for debugging:
const createLogger = (target, name) => {
return new Proxy(target, {
get(target, property) {
console.log(`[${name}] GET: ${property}`);
return target[property];
},
set(target, property, value) {
console.log(`[${name}] SET: ${property} = ${JSON.stringify(value)}`);
target[property] = value;
return true;
},
deleteProperty(target, property) {
console.log(`[${name}] DELETE: ${property}`);
delete target[property];
return true;
}
});
};
const user = createLogger({
name: "John",
age: 30
}, "User");
user.name; // [User] GET: name
user.age = 31; // [User] SET: age = 31
user.email = "john@example.com"; // [User] SET: email = "john@example.com"
delete user.age; // [User] DELETE: age
Virtual Properties with Proxies
Create computed properties that don't actually exist on the object:
const createVirtualProps = (target) => {
return new Proxy(target, {
get(target, property) {
// Real properties
if (property in target) {
return target[property];
}
// Virtual property: fullName
if (property === "fullName") {
return `${target.firstName} ${target.lastName}`;
}
// Virtual property: initials
if (property === "initials") {
return `${target.firstName[0]}.${target.lastName[0]}.`;
}
// Virtual property: age (calculated from birthYear)
if (property === "age") {
const currentYear = new Date().getFullYear();
return currentYear - target.birthYear;
}
return undefined;
}
});
};
const person = createVirtualProps({
firstName: "John",
lastName: "Doe",
birthYear: 1990
});
console.log(person.firstName); // "John"
console.log(person.fullName); // "John Doe" (virtual)
console.log(person.initials); // "J.D." (virtual)
console.log(person.age); // 36 (virtual, calculated)
Function Interceptors with Proxies
Intercept function calls and modify behavior:
const createTrackedFunction = (func, name) => {
let callCount = 0;
return new Proxy(func, {
apply(target, thisArg, args) {
callCount++;
console.log(`[${name}] Call #${callCount} with args:`, args);
const startTime = Date.now();
const result = target.apply(thisArg, args);
const duration = Date.now() - startTime;
console.log(`[${name}] Completed in ${duration}ms, result:`, result);
return result;
}
});
};
const add = (a, b) => a + b;
const trackedAdd = createTrackedFunction(add, "add");
trackedAdd(5, 3);
// [add] Call #1 with args: [5, 3]
// [add] Completed in 0ms, result: 8
trackedAdd(10, 20);
// [add] Call #2 with args: [10, 20]
// [add] Completed in 0ms, result: 30
The Reflect API
Reflect provides methods for interceptable JavaScript operations. It's often used with Proxies:
// Reflect methods mirror Proxy traps
const target = {
name: "John",
age: 30
};
// Reflect.get() - get property value
console.log(Reflect.get(target, "name")); // "John"
// Reflect.set() - set property value
Reflect.set(target, "age", 31);
console.log(target.age); // 31
// Reflect.has() - check if property exists
console.log(Reflect.has(target, "name")); // true
console.log(Reflect.has(target, "email")); // false
// Reflect.deleteProperty() - delete property
Reflect.deleteProperty(target, "age");
console.log(target.age); // undefined
// Reflect.ownKeys() - get all keys
const obj = { a: 1, b: 2 };
console.log(Reflect.ownKeys(obj)); // ["a", "b"]
// Reflect.apply() - call function
function greet(greeting) {
return `${greeting}, ${this.name}!`;
}
console.log(Reflect.apply(greet, { name: "John" }, ["Hello"])); // "Hello, John!"
Why Reflect? Reflect provides a cleaner, more consistent API for meta-programming operations. It returns success/failure as boolean rather than throwing errors.
Using Reflect in Proxy Traps
Reflect is commonly used within proxy traps to forward operations:
const handler = {
get(target, property, receiver) {
console.log(`Accessing property: ${property}`);
// Use Reflect to forward the operation
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`Setting ${property} to ${value}`);
// Use Reflect to forward the operation
return Reflect.set(target, property, value, receiver);
},
has(target, property) {
console.log(`Checking if ${property} exists`);
return Reflect.has(target, property);
}
};
const obj = new Proxy({ name: "John", age: 30 }, handler);
console.log(obj.name); // Accessing property: name, then "John"
obj.age = 31; // Setting age to 31
console.log("name" in obj); // Checking if name exists, then true
Negative Array Indices with Proxies
Implement Python-style negative indexing for arrays:
const createNegativeArray = (array) => {
return new Proxy(array, {
get(target, property) {
const index = Number(property);
// Handle negative indices
if (index < 0) {
return target[target.length + index];
}
return Reflect.get(target, property);
}
});
};
const arr = createNegativeArray(["a", "b", "c", "d", "e"]);
console.log(arr[0]); // "a"
console.log(arr[-1]); // "e" (last element)
console.log(arr[-2]); // "d" (second to last)
console.log(arr[-5]); // "a" (first element via negative index)
Read-Only Objects with Proxies
Create truly immutable objects:
const createReadOnly = (target) => {
return new Proxy(target, {
set(target, property, value) {
throw new Error(`Cannot modify read-only property: ${property}`);
},
deleteProperty(target, property) {
throw new Error(`Cannot delete read-only property: ${property}`);
},
defineProperty(target, property, descriptor) {
throw new Error(`Cannot define property on read-only object: ${property}`);
}
});
};
const config = createReadOnly({
apiUrl: "https://api.example.com",
timeout: 5000
});
console.log(config.apiUrl); // "https://api.example.com"
// These all throw errors:
// config.apiUrl = "new-url"; // Error: Cannot modify read-only property: apiUrl
// delete config.timeout; // Error: Cannot delete read-only property: timeout
// config.newProp = "value"; // Error: Cannot modify read-only property: newProp
Observable Pattern with Proxies
Implement reactive programming patterns:
const createObservable = (target, callback) => {
return new Proxy(target, {
set(target, property, value) {
const oldValue = target[property];
target[property] = value;
// Notify observers of the change
callback(property, oldValue, value);
return true;
}
});
};
const state = createObservable(
{ count: 0, name: "App" },
(property, oldValue, newValue) => {
console.log(`Property "${property}" changed from ${oldValue} to ${newValue}`);
}
);
state.count = 1;
// Property "count" changed from 0 to 1
state.count = 2;
// Property "count" changed from 1 to 2
state.name = "MyApp";
// Property "name" changed from App to MyApp
Real-World Use: This pattern is the foundation of reactive frameworks like Vue.js, which use Proxies for reactive data binding.
Practice Exercise:
Challenge: Create a smart cache system using Proxy with the following features:
- Track cache hits and misses
- Automatically compute and cache expensive operations
- Provide cache statistics (hits, misses, hit rate)
- Allow cache clearing
Solution:
const createSmartCache = (computeFunction) => {
const cache = new Map();
let hits = 0;
let misses = 0;
const handler = {
get(target, property) {
// Special methods
if (property === "stats") {
return () => ({
hits,
misses,
total: hits + misses,
hitRate: hits / (hits + misses) || 0,
cacheSize: cache.size
});
}
if (property === "clear") {
return () => {
cache.clear();
hits = 0;
misses = 0;
};
}
// Check cache
if (cache.has(property)) {
hits++;
console.log(`Cache HIT for: ${property}`);
return cache.get(property);
}
// Cache miss - compute value
misses++;
console.log(`Cache MISS for: ${property}`);
const value = computeFunction(property);
cache.set(property, value);
return value;
}
};
return new Proxy({}, handler);
};
// Example: Expensive fibonacci computation
const fibonacci = (n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const smartFib = createSmartCache((n) => fibonacci(Number(n)));
console.log(smartFib[10]); // Cache MISS for: 10, computes value
console.log(smartFib[10]); // Cache HIT for: 10, returns cached
console.log(smartFib[5]); // Cache MISS for: 5, computes value
console.log(smartFib[10]); // Cache HIT for: 10, returns cached
console.log(smartFib.stats());
// { hits: 2, misses: 2, total: 4, hitRate: 0.5, cacheSize: 2 }
smartFib.clear();
console.log(smartFib.stats());
// { hits: 0, misses: 0, total: 0, hitRate: 0, cacheSize: 0 }
Performance Considerations
Proxies add overhead to operations. Use them judiciously:
// Proxies have performance cost
const plainObject = { x: 1, y: 2 };
const proxiedObject = new Proxy({ x: 1, y: 2 }, {
get(target, property) {
return target[property];
}
});
// Benchmark
console.time("Plain object");
for (let i = 0; i < 1000000; i++) {
plainObject.x;
}
console.timeEnd("Plain object"); // ~3ms
console.time("Proxied object");
for (let i = 0; i < 1000000; i++) {
proxiedObject.x;
}
console.timeEnd("Proxied object"); // ~20ms (slower)
Performance Tip: Proxies are powerful but add overhead. Use them for developer tools, validation, and complex scenarios - not for hot paths in performance-critical code.
Summary
In this lesson, you learned:
- Proxies intercept and customize object operations
- Common traps: get, set, has, deleteProperty, apply, construct
- Use cases: validation, logging, virtual properties, observables
- Reflect API provides methods for forwarding operations
- Proxies enable meta-programming and reactive patterns
- Proxies have performance overhead - use appropriately
- Real-world applications in frameworks and tools
Congratulations! You've completed Module 5: Object-Oriented JavaScript! You now understand ES6 classes, inheritance, prototypes, object methods, and advanced proxy patterns. In the next module, we'll explore ES6 modules and code organization!