Metaprogramming: Reflect, Proxy, and Symbols
About 1470 wordsAbout 18 min
2025-08-05
This section explores JavaScript's metaprogramming capabilities, allowing programs to manipulate other programs or themselves at runtime.
Symbols: Unique and Private Properties
Symbols are primitive values that represent unique identifiers, perfect for adding private properties to objects.
Creating and Using Symbols
// Basic symbol creation
const idSymbol = Symbol('id');
const anotherIdSymbol = Symbol('id');
console.log(idSymbol === anotherIdSymbol); // false - each symbol is unique
// Symbols as object keys
const user = {
name: 'John',
[idSymbol]: 12345,
[Symbol('secret')]: 'hidden-value'
};
console.log(user[idSymbol]); // 12345
console.log(user[Symbol('secret')]); // undefined - different symbol
// Symbols are not enumerable in for...in loops
for (const key in user) {
console.log(key); // only 'name' is logged
}
// Object.keys() also ignores symbols
console.log(Object.keys(user)); // ['name']
// To get symbol properties, use Object.getOwnPropertySymbols()
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol()]
Well-Known Symbols
JavaScript provides built-in symbols for customizing object behavior:
// Symbol.iterator - for custom iteration
const iterableObject = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => ({
value: this.data[index++],
done: index > this.data.length
})
};
}
};
for (const item of iterableObject) {
console.log(item); // 1, 2, 3
}
// Symbol.toStringTag - customize Object.prototype.toString.call()
const customObject = {
[Symbol.toStringTag]: 'CustomObject'
};
console.log(Object.prototype.toString.call(customObject)); // [object CustomObject]
// Symbol.hasInstance - customize instanceof behavior
class CustomClass {
static [Symbol.hasInstance](instance) {
return instance.customProperty !== undefined;
}
}
const obj = { customProperty: 'test' };
console.log(obj instanceof CustomClass); // true
// Other well-known symbols:
// Symbol.toPrimitive - convert object to primitive
// Symbol.species - constructor for derived objects
// Symbol.match, Symbol.replace, Symbol.search, Symbol.split - string methods
// Symbol.isConcatSpreadable - Array.prototype.concat behavior
Proxy: Intercepting Object Operations
Proxy objects allow you to intercept and customize fundamental operations on objects.
Basic Proxy Creation
const target = {
name: 'John',
age: 30
};
const handler = {
get(target, property) {
console.log(`Getting property: ${property}`);
return target[property];
},
set(target, property, value) {
console.log(`Setting property: ${property} = ${value}`);
target[property] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
proxy.name = 'Jane'; // Logs: Setting property: name = Jane
console.log(proxy.name); // Logs: Getting property: name, then outputs: Jane
Proxy Traps (Handlers)
const advancedHandler = {
// Property access traps
get(target, property, receiver) {
console.log(`GET: ${property}`);
if (property in target) {
return target[property];
}
return `Property ${property} not found`;
},
set(target, property, value, receiver) {
console.log(`SET: ${property} = ${value}`);
if (typeof value === 'string' && value.length < 3) {
throw new Error('Value too short');
}
target[property] = value;
return true;
},
// Property existence traps
has(target, property) {
console.log(`HAS: ${property}`);
return property in target;
},
// Property deletion traps
deleteProperty(target, property) {
console.log(`DELETE: ${property}`);
if (property === 'immutable') {
return false; // Prevent deletion
}
delete target[property];
return true;
},
// Property enumeration traps
ownKeys(target) {
console.log('OWN KEYS');
return Object.keys(target).filter(key => key !== 'hidden');
},
// Property descriptor traps
getOwnPropertyDescriptor(target, property) {
console.log(`GET OWN PROPERTY DESCRIPTOR: ${property}`);
const descriptor = Object.getOwnPropertyDescriptor(target, property);
if (descriptor) {
descriptor.enumerable = true;
}
return descriptor;
},
// Function call traps
apply(target, thisArg, argumentsList) {
console.log(`APPLY: ${argumentsList}`);
return target.apply(thisArg, argumentsList);
},
// Constructor traps
construct(target, argumentsList, newTarget) {
console.log(`CONSTRUCT: ${argumentsList}`);
return new target(...argumentsList);
}
};
const advancedProxy = new Proxy({ name: 'John' }, advancedHandler);
Practical Proxy Use Cases
1. Validation Proxy:
function createValidationProxy(target, schema) {
return new Proxy(target, {
set(target, property, value) {
if (schema[property]) {
const validator = schema[property];
if (!validator(value)) {
throw new Error(`Invalid value for ${property}`);
}
}
target[property] = value;
return true;
}
});
}
const userSchema = {
name: (value) => typeof value === 'string' && value.length > 0,
age: (value) => typeof value === 'number' && value >= 0 && value <= 150,
email: (value) => typeof value === 'string' && value.includes('@')
};
const user = createValidationProxy({}, userSchema);
user.name = 'John'; // OK
user.age = 25; // OK
user.age = -5; // Error: Invalid value for age
2. Logging Proxy:
function createLoggingProxy(target) {
return new Proxy(target, {
get(target, property) {
if (typeof target[property] === 'function') {
return function(...args) {
console.log(`Calling ${property} with args:`, args);
const result = target[property].apply(this, args);
console.log(`${property} returned:`, result);
return result;
};
}
return target[property];
}
});
}
const api = createLoggingProxy({
getData: (id) => ({ id, data: 'sample' }),
saveData: (data) => true
});
api.getData(123); // Logs method call and return value
3. Memoization Proxy:
function createMemoizationProxy(target) {
const cache = new Map();
return new Proxy(target, {
apply(target, thisArg, argumentsList) {
const key = JSON.stringify(argumentsList);
if (cache.has(key)) {
console.log('Cache hit!');
return cache.get(key);
}
console.log('Cache miss, computing...');
const result = target.apply(thisArg, argumentsList);
cache.set(key, result);
return result;
}
});
}
const expensiveFunction = createMemoizationProxy((n) => {
// Simulate expensive computation
for (let i = 0; i < 1e6; i++) {}
return n * n;
});
console.log(expensiveFunction(5)); // Cache miss
console.log(expensiveFunction(5)); // Cache hit
Reflect: Unified Reflection API
Reflect provides a set of methods for interceptable JavaScript operations, serving as a more consistent alternative to traditional object operations.
Reflect Methods vs Traditional Operations
const obj = { name: 'John', age: 30 };
// Traditional vs Reflect comparison
console.log(obj.name); // Traditional
console.log(Reflect.get(obj, 'name')); // Reflect
obj.name = 'Jane'; // Traditional
Reflect.set(obj, 'name', 'Jane'); // Reflect
delete obj.age; // Traditional
Reflect.deleteProperty(obj, 'age'); // Reflect
'name' in obj; // Traditional
Reflect.has(obj, 'name'); // Reflect
Object.keys(obj); // Traditional
Reflect.ownKeys(obj); // Reflect
Key Reflect Methods
const target = {};
// Property operations
Reflect.set(target, 'name', 'John');
console.log(Reflect.get(target, 'name')); // John
Reflect.defineProperty(target, 'age', {
value: 30,
writable: true,
enumerable: true
});
// Function operations
function greet(name) {
return `Hello, ${name}!`;
}
console.log(Reflect.apply(greet, null, ['World'])); // Hello, World!
// Constructor operations
class Person {
constructor(name) {
this.name = name;
}
}
const person = Reflect.construct(Person, ['Alice']);
console.log(person instanceof Person); // true
// Property descriptor operations
const descriptor = Reflect.getOwnPropertyDescriptor(target, 'name');
console.log(descriptor);
// Prototype operations
const prototype = Reflect.getPrototypeOf(target);
console.log(prototype);
// Extensibility operations
console.log(Reflect.isExtensible(target));
Reflect.preventExtensions(target);
console.log(Reflect.isExtensible(target)); // false
Advanced Reflect Usage
1. Safe Property Access:
function safeGet(obj, property, defaultValue = undefined) {
try {
return Reflect.get(obj, property, defaultValue);
} catch (error) {
return defaultValue;
}
}
const user = { name: 'John', profile: { age: 30 } };
console.log(safeGet(user, 'name')); // John
console.log(safeGet(user, 'profile.age')); // undefined (nested access)
console.log(safeGet(user, 'nonexistent', 'default')); // default
2. Dynamic Method Invocation:
function invokeMethod(obj, methodName, ...args) {
const method = Reflect.get(obj, methodName);
if (typeof method === 'function') {
return Reflect.apply(method, obj, args);
}
throw new Error(`Method ${methodName} not found`);
}
const calculator = {
add(a, b) { return a + b; },
multiply(a, b) { return a * b; }
};
console.log(invokeMethod(calculator, 'add', 5, 3)); // 8
console.log(invokeMethod(calculator, 'multiply', 4, 6)); // 24
3. Proxy with Reflect:
function createObservable(target, callback) {
return new Proxy(target, {
set(target, property, value, receiver) {
const oldValue = Reflect.get(target, property, receiver);
const result = Reflect.set(target, property, value, receiver);
callback(property, oldValue, value);
return result;
},
deleteProperty(target, property) {
const oldValue = Reflect.get(target, property);
const result = Reflect.deleteProperty(target, property);
callback(property, oldValue, undefined);
return result;
}
});
}
const state = createObservable(
{ count: 0, name: 'John' },
(property, oldValue, newValue) => {
console.log(`${property} changed from ${oldValue} to ${newValue}`);
}
);
state.count = 1; // Logs: count changed from 0 to 1
state.name = 'Jane'; // Logs: name changed from John to Jane
Advanced Metaprogramming Patterns
1. Aspect-Oriented Programming
function addLoggingAspect(target) {
const handler = {
get(target, property, receiver) {
const original = Reflect.get(target, property, receiver);
if (typeof original === 'function') {
return function(...args) {
console.log(`Entering ${property}`);
const result = Reflect.apply(original, this, args);
console.log(`Exiting ${property}`);
return result;
};
}
return original;
}
};
return new Proxy(target, handler);
}
function addTimingAspect(target) {
const handler = {
get(target, property, receiver) {
const original = Reflect.get(target, property, receiver);
if (typeof original === 'function') {
return function(...args) {
const start = performance.now();
const result = Reflect.apply(original, this, args);
const end = performance.now();
console.log(`${property} took ${end - start}ms`);
return result;
};
}
return original;
}
};
return new Proxy(target, handler);
}
class Service {
processData(data) {
// Simulate work
for (let i = 0; i < 1e6; i++) {}
return data.toUpperCase();
}
}
const service = addTimingAspect(addLoggingAspect(new Service()));
service.processData('hello'); // Logs timing and method calls
2. Immutable Objects
function createImmutableObject(target) {
return new Proxy(target, {
set(target, property, value) {
throw new Error(`Cannot set property ${property} on immutable object`);
},
deleteProperty(target, property) {
throw new Error(`Cannot delete property ${property} on immutable object`);
},
defineProperty(target, property, descriptor) {
throw new Error(`Cannot define property ${property} on immutable object`);
},
setPrototypeOf(target, prototype) {
throw new Error('Cannot change prototype of immutable object');
}
});
}
const immutable = createImmutableObject({ name: 'John', age: 30 });
immutable.name = 'Jane'; // Error: Cannot set property name on immutable object
3. Reactive Programming
class ReactiveSystem {
constructor() {
this.data = new Map();
this.dependencies = new Map();
this.subscribers = new Map();
}
reactive(key, computeFn) {
this.data.set(key, computeFn());
return new Proxy({}, {
get: (target, property) => {
if (property === 'value') {
return this.data.get(key);
}
return undefined;
},
set: (target, property, value) => {
if (property === 'value') {
this.data.set(key, value);
this.notifySubscribers(key);
return true;
}
return false;
}
});
}
computed(key, computeFn, dependencies) {
this.dependencies.set(key, dependencies);
// Recompute when dependencies change
dependencies.forEach(dep => {
this.subscribe(dep, () => {
const newValue = computeFn();
this.data.set(key, newValue);
this.notifySubscribers(key);
});
});
return this.reactive(key, computeFn);
}
subscribe(key, callback) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set());
}
this.subscribers.get(key).add(callback);
}
notifySubscribers(key) {
if (this.subscribers.has(key)) {
this.subscribers.get(key).forEach(callback => callback());
}
}
}
// Usage
const reactive = new ReactiveSystem();
const name = reactive.reactive('name', () => 'John');
const greeting = reactive.computed('greeting',
() => `Hello, ${name.value}!`,
['name']
);
console.log(greeting.value); // Hello, John!
name.value = 'Jane';
console.log(greeting.value); // Hello, Jane!
Changelog
2aa48
-web-deploy(Auto): Update base URL for web-pages branchon
Copyright
Copyright Ownership:WARREN Y.F. LONG
License under:Attribution-NonCommercial-NoDerivatives 4.0 International (CC-BY-NC-ND-4.0)