Reflect API: Metaprogramming Fundamentals in JavaScript
The Reflect API provides methods for interceptable JavaScript operations. This article covers reflection, introspection, and practical metaprogramming patterns.
Introduction
The Reflect API enables:
- Introspection of objects
- Dynamic property access
- Method invocation
- Object construction
- Metaprogramming patterns
Understanding Reflect helps you:
- Build advanced abstractions
- Implement frameworks
- Create dynamic systems
- Understand language internals
Reflect API Basics
Reflect Methods
// Reflect provides static methods for common operations
// Each method corresponds to a language operation
// Reflect.get() - Get property value
const obj = { name: 'John', age: 30 };
console.log(Reflect.get(obj, 'name')); // 'John'
// Reflect.set() - Set property value
Reflect.set(obj, 'age', 31);
console.log(obj.age); // 31
// Reflect.has() - Check if property exists
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'email')); // false
// Reflect.deleteProperty() - Delete property
Reflect.deleteProperty(obj, 'age');
console.log(Reflect.has(obj, 'age')); // false
Reflect vs Traditional Operations
// Traditional way
const obj = { x: 1 };
obj.x = 2;
delete obj.x;
// Reflect way
const obj = { x: 1 };
Reflect.set(obj, 'x', 2);
Reflect.deleteProperty(obj, 'x');
// Reflect returns boolean indicating success
const result = Reflect.set(obj, 'x', 2);
console.log(result); // true
Property Descriptors
Getting Property Descriptors
// โ
Good: Get property descriptor
const obj = { name: 'John' };
const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// {
// value: 'John',
// writable: true,
// enumerable: true,
// configurable: true
// }
Defining Property Descriptors
// โ
Good: Define property with descriptor
const obj = {};
Reflect.defineProperty(obj, 'name', {
value: 'John',
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.name); // 'John'
obj.name = 'Jane'; // Fails silently in non-strict mode
console.log(obj.name); // 'John'
Practical Property Descriptor Example
// โ
Good: Create read-only properties
class User {
constructor(name, email) {
Reflect.defineProperty(this, 'name', {
value: name,
writable: false,
enumerable: true,
configurable: false
});
Reflect.defineProperty(this, 'email', {
value: email,
writable: false,
enumerable: true,
configurable: false
});
}
}
const user = new User('John', '[email protected]');
console.log(user.name); // 'John'
user.name = 'Jane'; // Fails silently
console.log(user.name); // 'John'
Object Introspection
Getting Object Information
// โ
Good: Introspect object properties
const obj = { a: 1, b: 2, c: 3 };
// Get all own property names
const keys = Reflect.ownKeys(obj);
console.log(keys); // ['a', 'b', 'c']
// Get property names (enumerable only)
const enumerableKeys = Object.keys(obj);
console.log(enumerableKeys); // ['a', 'b', 'c']
// Get all property names including non-enumerable
const allKeys = Reflect.ownKeys(obj);
console.log(allKeys); // ['a', 'b', 'c']
Checking Object Characteristics
// โ
Good: Check object characteristics
const obj = { x: 1 };
// Check if extensible (can add properties)
console.log(Reflect.isExtensible(obj)); // true
// Prevent extensions
Reflect.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // false
// Try to add property
const result = Reflect.set(obj, 'y', 2);
console.log(result); // false (failed)
console.log(obj.y); // undefined
Function and Constructor Reflection
Calling Functions
// โ
Good: Call function with Reflect
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
// Traditional way
const result1 = greet('Hello', 'John');
// Reflect way
const result2 = Reflect.apply(greet, null, ['Hello', 'John']);
console.log(result1); // 'Hello, John!'
console.log(result2); // 'Hello, John!'
Constructing Objects
// โ
Good: Construct objects with Reflect
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// Traditional way
const user1 = new User('John', '[email protected]');
// Reflect way
const user2 = Reflect.construct(User, ['John', '[email protected]']);
console.log(user1); // User { name: 'John', email: '[email protected]' }
console.log(user2); // User { name: 'John', email: '[email protected]' }
Practical Constructor Example
// โ
Good: Dynamic object construction
function createInstance(Constructor, args) {
if (!Reflect.isExtensible(Constructor.prototype)) {
throw new Error('Constructor prototype is not extensible');
}
return Reflect.construct(Constructor, args);
}
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
const product = createInstance(Product, ['Laptop', 999]);
console.log(product); // Product { name: 'Laptop', price: 999 }
Prototype Chain Introspection
Getting and Setting Prototype
// โ
Good: Introspect prototype chain
const obj = { x: 1 };
// Get prototype
const proto = Reflect.getPrototypeOf(obj);
console.log(proto); // Object.prototype
// Set prototype
const newProto = { y: 2 };
Reflect.setPrototypeOf(obj, newProto);
console.log(obj.y); // 2 (inherited from new prototype)
Practical Prototype Example
// โ
Good: Create object with specific prototype
const animalProto = {
speak() {
return `${this.name} makes a sound`;
}
};
const dogProto = Reflect.create(animalProto, {
bark: {
value() {
return `${this.name} barks`;
}
}
});
const dog = Reflect.create(dogProto);
dog.name = 'Rex';
console.log(dog.speak()); // 'Rex makes a sound'
console.log(dog.bark()); // 'Rex barks'
Practical Metaprogramming Patterns
Object Validation
// โ
Good: Validate object structure
function validateObject(obj, schema) {
for (const [key, type] of Object.entries(schema)) {
if (!Reflect.has(obj, key)) {
throw new Error(`Missing property: ${key}`);
}
const value = Reflect.get(obj, key);
if (typeof value !== type) {
throw new Error(`Invalid type for ${key}: expected ${type}, got ${typeof value}`);
}
}
return true;
}
// Usage
const schema = {
name: 'string',
age: 'number',
email: 'string'
};
const user = { name: 'John', age: 30, email: '[email protected]' };
validateObject(user, schema); // OK
const invalid = { name: 'John', age: '30' };
validateObject(invalid, schema); // Error: Invalid type for age
Object Cloning
// โ
Good: Deep clone object using Reflect
function deepClone(obj, visited = new WeakSet()) {
if (visited.has(obj)) return obj; // Circular reference
visited.add(obj);
const cloned = Reflect.construct(Object, []);
for (const key of Reflect.ownKeys(obj)) {
const descriptor = Reflect.getOwnPropertyDescriptor(obj, key);
const value = Reflect.get(obj, key);
if (typeof value === 'object' && value !== null) {
descriptor.value = deepClone(value, visited);
}
Reflect.defineProperty(cloned, key, descriptor);
}
return cloned;
}
// Usage
const original = { a: 1, b: { c: 2 } };
const clone = deepClone(original);
clone.b.c = 3;
console.log(original.b.c); // 2 (unchanged)
console.log(clone.b.c); // 3
Object Serialization
// โ
Good: Serialize object using Reflect
function serialize(obj) {
const result = {};
for (const key of Reflect.ownKeys(obj)) {
const descriptor = Reflect.getOwnPropertyDescriptor(obj, key);
if (descriptor.enumerable) {
const value = Reflect.get(obj, key);
if (typeof value === 'object' && value !== null) {
result[key] = serialize(value);
} else if (typeof value !== 'function') {
result[key] = value;
}
}
}
return result;
}
// Usage
const user = {
name: 'John',
age: 30,
email: '[email protected]',
greet() {
return 'Hello';
}
};
const serialized = serialize(user);
console.log(JSON.stringify(serialized));
// {"name":"John","age":30,"email":"[email protected]"}
Mixin Pattern
// โ
Good: Apply mixins using Reflect
function applyMixin(target, mixin) {
for (const key of Reflect.ownKeys(mixin)) {
if (key !== 'constructor') {
const descriptor = Reflect.getOwnPropertyDescriptor(mixin, key);
Reflect.defineProperty(target, key, descriptor);
}
}
return target;
}
// Usage
const canEat = {
eat() {
return `${this.name} is eating`;
}
};
const canWalk = {
walk() {
return `${this.name} is walking`;
}
};
class Animal {
constructor(name) {
this.name = name;
}
}
applyMixin(Animal.prototype, canEat);
applyMixin(Animal.prototype, canWalk);
const dog = new Animal('Rex');
console.log(dog.eat()); // 'Rex is eating'
console.log(dog.walk()); // 'Rex is walking'
Best Practices
-
Use Reflect for consistency:
// โ Good Reflect.set(obj, 'x', 1); Reflect.get(obj, 'x'); -
Check return values:
// โ Good const success = Reflect.set(obj, 'x', 1); if (!success) { console.error('Failed to set property'); } -
Use for metaprogramming:
// โ Good const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'x'); -
Combine with Proxy:
// โ Good const handler = { get(target, prop) { return Reflect.get(target, prop); } };
Common Mistakes
-
Ignoring return values:
// โ Bad Reflect.set(obj, 'x', 1); // โ Good const success = Reflect.set(obj, 'x', 1); -
Not handling errors:
// โ Bad Reflect.construct(Constructor, args); // โ Good try { Reflect.construct(Constructor, args); } catch (error) { console.error('Construction failed:', error); } -
Mixing Reflect and traditional operations:
// โ Bad - inconsistent obj.x = 1; Reflect.get(obj, 'y'); // โ Good - consistent Reflect.set(obj, 'x', 1); Reflect.get(obj, 'y');
Summary
The Reflect API is powerful for metaprogramming. Key takeaways:
- Reflect provides methods for language operations
- Use for introspection and dynamic access
- Check return values for success/failure
- Combine with Proxy for advanced patterns
- Enables metaprogramming and frameworks
- Consistent API for object manipulation
Related Resources
Next Steps
- Learn about Proxy Objects: Interception and Traps
- Explore Symbol: Unique Identifiers
- Study Dynamic Code Evaluation
- Practice metaprogramming patterns
- Build advanced abstractions
Comments