Reflect and Proxy
Reflect API
The Reflect API provides methods for interceptable JavaScript operations. It's a global object that helps with forwarding default operations from the handler to the target.
Core Reflect Methods
1. Reflect.get()
const person = { name: 'John' };
console.log(Reflect.get(person, 'name')); // 'John'
// With receiver (this binding)
const user = {
_name: 'Alice',
get name() {
return this._name;
}
};
const proxy = new Proxy(user, {});
console.log(Reflect.get(user, 'name', proxy)); // 'Alice'
2. Reflect.set()
const person = { age: 30 };
Reflect.set(person, 'age', 31);
console.log(person.age); // 31
// With receiver
const user = {
_age: 25,
set age(value) {
this._age = value;
}
};
Reflect.set(user, 'age', 26, user);
3. Reflect.has()
const person = { name: 'John' };
console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.has(person, 'age')); // false
4. Reflect.deleteProperty()
const person = { name: 'John', age: 30 };
Reflect.deleteProperty(person, 'age');
console.log(person); // { name: 'John' }
5. Reflect.construct()
function Person(name) {
this.name = name;
}
const person = Reflect.construct(Person, ['John']);
console.log(person.name); // 'John'
Proxy API
Proxy allows you to create an object that can intercept and redefine fundamental operations for another object (like property lookup, assignment, enumeration, function invocation, etc.).
Common Use Cases
1. Validation
const validator = {
set(target, property, value) {
if (property === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Age must be an integer');
}
if (value < 0 || value > 120) {
throw new RangeError('Age must be between 0 and 120');
}
}
return Reflect.set(target, property, value);
}
};
const person = new Proxy({}, validator);
person.age = 30; // Works
// person.age = -1; // Throws RangeError
// person.age = 'young'; // Throws TypeError
2. Logging
const handler = {
get(target, property) {
console.log(`Accessing property: ${property}`);
return Reflect.get(target, property);
},
set(target, property, value) {
console.log(`Setting property: ${property} = ${value}`);
return Reflect.set(target, property, value);
}
};
const user = new Proxy({}, handler);
user.name = 'John'; // Logs: "Setting property: name = John"
console.log(user.name); // Logs: "Accessing property: name", then "John"
3. Default Values
const handler = {
get(target, property) {
return Reflect.get(target, property) ?? 'Property not found';
}
};
const obj = new Proxy({}, handler);
console.log(obj.nonexistent); // 'Property not found'
4. Read-Only Properties
const handler = {
set(target, property) {
throw new Error(`Property ${property} is read-only`);
}
};
const readOnly = new Proxy({ data: 'protected' }, handler);
// readOnly.data = 'new value'; // Throws Error
5. Private Properties
const handler = {
get(target, property) {
if (property.startsWith('_')) {
throw new Error('Access to private property denied');
}
return Reflect.get(target, property);
},
set(target, property, value) {
if (property.startsWith('_')) {
throw new Error('Cannot modify private property');
}
return Reflect.set(target, property, value);
}
};
const obj = new Proxy({
_private: 'secret',
public: 'accessible'
}, handler);
console.log(obj.public); // 'accessible'
// console.log(obj._private); // Throws Error
Advanced Patterns
Method Decorators with Proxy
function measureTime(target) {
const handler = {
apply(target, thisArg, args) {
const start = performance.now();
const result = Reflect.apply(target, thisArg, args);
const end = performance.now();
console.log(`Execution time: ${end - start}ms`);
return result;
}
};
return new Proxy(target, handler);
}
const slowFunction = measureTime(function(n) {
let result = 0;
for(let i = 0; i < n; i++) {
result += i;
}
return result;
});
slowFunction(1000000); // Logs execution time
Reactive Properties
function reactive(obj) {
return new Proxy(obj, {
set(target, property, value) {
const oldValue = target[property];
const result = Reflect.set(target, property, value);
if (oldValue !== value) {
console.log(`Property ${property} changed from ${oldValue} to ${value}`);
// Could trigger UI updates or other reactions here
}
return result;
}
});
}
const state = reactive({
count: 0
});
state.count++; // Logs: "Property count changed from 0 to 1"
Best Practices
-
Always Use Reflect with Proxy: When implementing Proxy handlers, use Reflect methods instead of direct object operations to maintain proper behavior and handle edge cases correctly.
-
Handle Edge Cases: Consider what happens with special properties like Symbol.iterator, toString, etc.
-
Preserve Invariants: Make sure your Proxy handlers maintain JavaScript's fundamental invariants (e.g., non-configurable properties can't be deleted).
-
Performance Considerations: Be mindful that Proxies add overhead. Use them judiciously, especially in performance-critical code.
Common Pitfalls
-
this Binding: Be careful with methods that use
this
. Thethis
value inside a method might refer to the Proxy instead of the target object. -
Prototype Chain: Remember that Proxies don't automatically proxy the prototype chain.
-
Non-Extensible Objects: Some operations are restricted on non-extensible objects, sealed objects, or frozen objects.
// Example of this binding issue
const target = {
name: 'John',
getName() {
return this.name;
}
};
const handler = {
get(target, property) {
return Reflect.get(target, property);
}
};
const proxy = new Proxy(target, handler);
const { getName } = proxy; // Destructuring breaks this binding
console.log(getName()); // undefined - this is not bound correctly
console.log(proxy.getName()); // 'John' - works correctly
Additional Reflection Methods
Object.setPrototypeOf() and Reflect.setPrototypeOf()
// Using Object.setPrototypeOf
const animal = {
makeSound() {
return 'Some sound';
}
};
const dog = {
bark() {
return 'Woof!';
}
};
Object.setPrototypeOf(dog, animal);
// OR
Reflect.setPrototypeOf(dog, animal);
console.log(dog.makeSound()); // 'Some sound'
console.log(dog.bark()); // 'Woof!'
// Checking prototype chain
console.log(Object.getPrototypeOf(dog) === animal); // true
Object.getPrototypeOf() and Reflect.getPrototypeOf()
class Parent {
parentMethod() {
return 'Parent method';
}
}
class Child extends Parent {
childMethod() {
return 'Child method';
}
}
const child = new Child();
console.log(Object.getPrototypeOf(child) === Child.prototype); // true
console.log(Reflect.getPrototypeOf(child) === Child.prototype); // true
// Getting full prototype chain
function getPrototypeChain(obj) {
const chain = [];
let currentProto = Object.getPrototypeOf(obj);
while (currentProto !== null) {
chain.push(currentProto);
currentProto = Object.getPrototypeOf(currentProto);
}
return chain;
}
console.log(getPrototypeChain(child)); // [Child.prototype, Parent.prototype, Object.prototype]
Object.defineProperty() and Reflect.defineProperty()
const person = {};
// Using Object.defineProperty
Object.defineProperty(person, 'name', {
value: 'John',
writable: false,
enumerable: true,
configurable: false
});
// Using Reflect.defineProperty
Reflect.defineProperty(person, 'age', {
value: 30,
writable: true,
enumerable: true,
configurable: true
});
// Advanced usage with getters and setters
let internalValue = 0;
Reflect.defineProperty(person, 'score', {
get() {
return internalValue;
},
set(value) {
if (value < 0) throw new Error('Score cannot be negative');
internalValue = value;
},
enumerable: true,
configurable: true
});
// Usage
person.score = 100;
console.log(person.score); // 100
// person.score = -1; // Throws Error
Object.getOwnPropertyNames() and Object.getOwnPropertySymbols()
const sym1 = Symbol('sym1');
const sym2 = Symbol('sym2');
const obj = {
prop1: 'value1',
prop2: 'value2',
[sym1]: 'symbol value 1',
[sym2]: 'symbol value 2'
};
// Get all string property names
console.log(Object.getOwnPropertyNames(obj)); // ['prop1', 'prop2']
// Get all symbol properties
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(sym1), Symbol(sym2)]
// Combining both
const allProps = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj)
];
// Getting property descriptors
const fullPropDetails = allProps.reduce((acc, prop) => {
acc[prop] = Object.getOwnPropertyDescriptor(obj, prop);
return acc;
}, {});
Object.getOwnPropertyDescriptor() and Reflect.getOwnPropertyDescriptor()
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'John',
writable: false,
enumerable: true,
configurable: false
});
// Using Object method
const descriptor1 = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor1);
// {
// value: 'John',
// writable: false,
// enumerable: true,
// configurable: false
// }
// Using Reflect method
const descriptor2 = Reflect.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor2); // Same output as above
Practical Use Cases
Creating an Immutable Object
function makeImmutable(obj) {
const props = Object.getOwnPropertyNames(obj);
const symbols = Object.getOwnPropertySymbols(obj);
[...props, ...symbols].forEach(prop => {
Object.defineProperty(obj, prop, {
writable: false,
configurable: false
});
});
Object.preventExtensions(obj);
return obj;
}
const config = makeImmutable({
apiKey: '12345',
endpoint: 'https://api.example.com'
});
// config.apiKey = 'new key'; // Throws error in strict mode
// config.newProp = 'value'; // Throws error
Property Observer Pattern
function observeProperty(obj, prop, callback) {
let value = obj[prop];
Object.defineProperty(obj, prop, {
get() {
return value;
},
set(newValue) {
const oldValue = value;
value = newValue;
callback(prop, oldValue, newValue);
},
enumerable: true,
configurable: true
});
}
const user = {
name: 'John',
age: 30
};
observeProperty(user, 'name', (prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
});
user.name = 'Jane'; // Logs: "name changed from John to Jane"
Safe Property Access with Reflect
function safeGetProperty(obj, prop) {
if (Reflect.has(obj, prop)) {
return {
value: Reflect.get(obj, prop),
exists: true
};
}
return {
value: undefined,
exists: false
};
}
const data = {
user: {
name: 'John'
}
};
console.log(safeGetProperty(data, 'user')); // { value: { name: 'John' }, exists: true }
console.log(safeGetProperty(data, 'age')); // { value: undefined, exists: false }
These methods provide powerful tools for metaprogramming in JavaScript, allowing you to:
- Manipulate object prototypes
- Define and modify property attributes
- Inspect object properties and their descriptors
- Create advanced patterns like property observation and immutability
Remember that while these methods are powerful, they should be used judiciously as they can make code harder to understand and maintain if overused.