52. Proxy & Reflect: Metaprogramming in JavaScript
Introduction
A Proxy is an object that wraps another object and allows you to intercept and modify fundamental operations on that object, such as reading properties, writing properties, and calling functions. Reflect is an API that provides methods corresponding to Proxy handler traps.
For IT students, Proxy is very useful for automatic data validation, property access logging, and building reactive systems without a framework.
Why Are Proxy and Reflect Important?
When building a campus application, you need automatic validation on input forms, logging for debugging, and access control over sensitive data. Proxy provides an elegant way to do all of this without modifying the object's original code.
Basic Proxy
// Creating a Proxy
const target = {
name: 'Mike',
studentId: '1234567890',
gpa: 3.75
};
const handler = {
// Trap for getting a property
get(obj, prop) {
console.log(`Accessing property: ${prop}`);
return prop in obj ? obj[prop] : `Property '${prop}' not found`;
},
// Trap for setting a property
set(obj, prop, value) {
console.log(`Mengatur ${prop} = ${value}`);
obj[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Log: Accessing property: name -> 'Mike'
console.log(proxy.missing); // Log: Accessing property: missing -> "Property 'missing' not found"
proxy.gpa = 3.80; // Log: Setting gpa = 3.80
console.log(proxy.gpa); // Log: Accessing property: gpa -> 3.80
Handler Traps
const studentHandler = {
// get(target, property, receiver)
get(target, property, receiver) {
if (property === 'fullName') {
return `${target.name} (${target.studentId})`;
}
if (property === 'status') {
return target.gpa >= 2.5 ? 'Active' : 'Academic Warning';
}
return Reflect.get(target, property, receiver);
},
// set(target, property, value, receiver)
set(target, property, value, receiver) {
// GPA (GPA) validation
if (property === 'gpa') {
if (typeof value !== 'number' || value < 0 || value > 4) {
throw new Error('GPA must be between 0 and 4');
}
}
// Student ID (StudentID) validation
if (property === 'studentId') {
if (!/^\d{10}$/.test(value)) {
throw new Error('StudentID must be 10 digits');
}
}
console.log(`Setting ${property} = ${value}`);
return Reflect.set(target, property, value, receiver);
},
// has(target, property) - for the 'in' operator
has(target, property) {
console.log(`Checking property: ${property}`);
return property in target;
},
// deleteProperty(target, property) - for delete
deleteProperty(target, property) {
console.log(`Deleting property: ${property}`);
if (property === 'studentId') {
throw new Error('StudentID cannot be deleted');
}
return Reflect.deleteProperty(target, property);
},
// ownKeys(target) - for Object.keys(), Object.getOwnPropertyNames()
ownKeys(target) {
console.log('Getting all keys');
return Reflect.ownKeys(target);
},
// getOwnPropertyDescriptor(target, property)
getOwnPropertyDescriptor(target, property) {
const desc = Reflect.getOwnPropertyDescriptor(target, property);
if (desc) {
desc.configurable = true;
}
return desc;
}
};
const student = new Proxy({
name: 'Mike',
studentId: '1234567890',
gpa: 3.75
}, studentHandler);
console.log(student.fullName); // 'Mike (1234567890)'
console.log(student.status); // 'Active'
console.log('gpa' in student); // true
Apply Trap (for functions)
function add(a, b) {
return a + b;
}
const applyHandler = {
// apply(target, thisArg, argumentsList)
apply(target, thisArg, args) {
console.log(`Calling ${target.name} with args:`, args);
// Validate input
for (const arg of args) {
if (typeof arg !== 'number') {
throw new Error('All arguments must be numbers');
}
}
const result = Reflect.apply(target, thisArg, args);
console.log(`Result: ${result}`);
return result;
}
};
const addProxy = new Proxy(add, applyHandler);
console.log(addProxy(3, 5)); // Log: Calling add... -> 8
console.log(addProxy(10, 20)); // Log: Calling add... -> 30
// addProxy('a', 'b'); // Error: All arguments must be numbers
Construct Trap (for new)
class Student {
constructor(name, studentId) {
this.name = name;
this.studentId = studentId;
}
}
const constructHandler = {
// construct(target, argumentsList, newTarget)
construct(target, args, newTarget) {
console.log(`Creating new instance:`, args);
// Validate
if (args.length < 2) {
throw new Error('Name and StudentID are required');
}
const instance = Reflect.construct(target, args, newTarget);
// Post-processing
instance.createdAt = new Date().toISOString();
return instance;
}
};
const StudentProxy = new Proxy(Student, constructHandler);
const student = new StudentProxy('Mike', '1234567890');
console.log(student); // { name: 'Mike', studentId: '1234567890', createdAt: '...' }
console.log(student instanceof Student); // true
Revocable Proxy
// A revocable Proxy can be "turned off" permanently
const { proxy: secureData, revoke } = Proxy.revocable(
{ data: 'Sensitive student data' },
{
get(target, prop) {
if (prop === 'data') {
return target[prop];
}
return undefined;
}
}
);
console.log(secureData.data); // 'Sensitive student data'
// After revoke, access will throw an error
revoke();
// console.log(secureData.data); // TypeError: Cannot perform 'get' on a proxy that has been revoked
// Example: temporary access token
function createTemporaryAccess(data, durationMs) {
const { proxy, revoke } = Proxy.revocable(data, {
get(target, prop) {
return target[prop];
},
set(target, prop, value) {
target[prop] = value;
return true;
}
});
// Auto-revoke after a certain amount of time
setTimeout(revoke, durationMs);
return proxy;
}
// Access the data for 5 seconds
const tempAccess = createTemporaryAccess(
{ grade: [85, 90, 88] },
5000
);
console.log(tempAccess.grade); // [85, 90, 88]
// After 5 seconds, access will fail
Reflect API
// Reflect provides methods that correspond to Proxy traps
const obj = { a: 1, b: 2, c: 3 };
// get - getting a property
console.log(Reflect.get(obj, 'a')); // 1
// set - setting a property
Reflect.set(obj, 'd', 4);
console.log(obj.d); // 4
// has - checking for the presence of a property
console.log(Reflect.has(obj, 'a')); // true
// deleteProperty - deleting a property
Reflect.deleteProperty(obj, 'c');
console.log(obj.c); // undefined
// ownKeys - getting all keys
console.log(Reflect.ownKeys(obj)); // ['a', 'b', 'd']
// getOwnPropertyDescriptor
const desc = Reflect.getOwnPropertyDescriptor(obj, 'a');
console.log(desc); // { value: 1, writable: true, enumerable: true, configurable: true }
// defineProperty - defining a new property
Reflect.defineProperty(obj, 'e', {
value: 5,
writable: false,
enumerable: false
});
// getPrototypeOf - getting the prototype
console.log(Reflect.getPrototypeOf(obj)); // Object.prototype
// setPrototypeOf - setting the prototype
const proto = { greet() { return 'Hello!'; } };
Reflect.setPrototypeOf(obj, proto);
console.log(obj.greet()); // 'Hello!'
// apply - calling a function
function add(a, b) { return a + b; }
console.log(Reflect.apply(add, null, [3, 5])); // 8
// construct - creating a new instance
console.log(Reflect.construct(Student, ['Mike', '001']));
Real-World Example: Validation Proxy for Form Data
// =====================================================
// Auto-validation Proxy for Student Data
// =====================================================
function createValidatedStudent(data) {
const validators = {
name: (v) => typeof v === 'string' && v.length >= 3,
studentId: (v) => /^\d{10}$/.test(v),
email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
gpa: (v) => typeof v === 'number' && v >= 0 && v <= 4,
year: (v) => typeof v === 'number' && v >= 2000 && v <= new Date().getFullYear(),
majorCode: (v) => ['CS', 'IS', 'CE', 'MMA'].includes(v)
};
const errorMessages = {
name: 'Name must be at least 3 characters',
studentId: 'StudentID must be 10 digits',
email: 'Invalid email format',
gpa: 'GPA must be between 0.00 - 4.00',
year: 'Enrollment year is invalid',
majorCode: 'Invalid major'
};
const errors = new Map();
return new Proxy(data, {
set(target, prop, value) {
if (prop in validators) {
const isValid = validators[prop](value);
if (!isValid) {
errors.set(prop, errorMessages[prop]);
console.error(`Validation failed: ${prop} - ${errorMessages[prop]}`);
return false;
}
errors.delete(prop);
}
target[prop] = value;
return true;
},
get(target, prop) {
if (prop === 'isValid') {
return errors.size === 0;
}
if (prop === 'errors') {
return Object.fromEntries(errors);
}
if (prop === 'toJSON') {
return () => ({ ...target });
}
return target[prop];
}
});
}
// Usage
const student = createValidatedStudent({
name: 'Mike',
studentId: '1234567890',
email: '[email protected]',
gpa: 3.75,
year: 2021,
majorCode: 'CS'
});
console.log(student.isValid); // true
console.log(student.errors); // {}
// Update with validation
student.gpa = 3.80; // OK
student.gpa = 5; // Failed: GPA must be between 0.00 - 4.00
student.studentId = '123'; // Failed: StudentID must be 10 digits
console.log(student.isValid); // false
console.log(student.errors);
// { gpa: 'GPA must be between 0.00 - 4.00', studentId: 'StudentID must be 10 digits' }
Example: Logging Property Access
// =====================================================
// Audit Trail Proxy
// =====================================================
function createAuditProxy(target, auditLog) {
const trapNames = ['get', 'set', 'has', 'deleteProperty'];
const handler = {};
handler.get = function(obj, prop, receiver) {
// Internal properties are not logged
if (prop.startsWith('_') || prop === 'getAuditLog') {
return Reflect.get(obj, prop, receiver);
}
auditLog.push({
action: 'GET',
property: prop,
value: obj[prop],
timestamp: new Date().toISOString(),
stack: new Error().stack.split('\n')[2]?.trim()
});
return Reflect.get(obj, prop, receiver);
};
handler.set = function(obj, prop, value, receiver) {
const oldValue = obj[prop];
auditLog.push({
action: 'SET',
property: prop,
oldValue,
newValue: value,
timestamp: new Date().toISOString()
});
return Reflect.set(obj, prop, value, receiver);
};
handler.has = function(obj, prop) {
auditLog.push({
action: 'HAS',
property: prop,
timestamp: new Date().toISOString()
});
return Reflect.has(obj, prop);
};
handler.deleteProperty = function(obj, prop) {
auditLog.push({
action: 'DELETE',
property: prop,
oldValue: obj[prop],
timestamp: new Date().toISOString()
});
return Reflect.deleteProperty(obj, prop);
};
// Add methods to access the audit log
target.getAuditLog = () => [...auditLog];
target.clearAuditLog = () => { auditLog.length = 0; };
return new Proxy(target, handler);
}
// Usage
const studentData = createAuditProxy(
{ name: 'Mike', studentId: '001', gpa: 3.5 },
[]
);
// Operations that are logged
console.log(studentData.name); // GET name
studentData.gpa = 3.7; // SET gpa
console.log('studentId' in studentData); // HAS studentId
// View the audit log
console.log(studentData.getAuditLog());
Example: Reactive State Management
// =====================================================
// Simple Reactive System with Proxy
// =====================================================
function createReactive(initialState, onChange) {
const state = { ...initialState };
return new Proxy(state, {
set(target, prop, value) {
const oldValue = target[prop];
target[prop] = value;
if (oldValue !== value) {
onChange(prop, value, oldValue);
}
return true;
},
get(target, prop) {
return target[prop];
}
});
}
// Usage: UI binding
const uiState = createReactive(
{ count: 0, message: 'Welcome' },
(prop, newValue, oldValue) => {
console.log(`${prop} changed: ${oldValue} -> ${newValue}`);
// Update DOM
const el = document.getElementById(`display-${prop}`);
if (el) {
el.textContent = newValue;
}
}
);
// Every change automatically updates the UI
uiState.count = 1; // 'count changed: 0 -> 1' + update DOM
uiState.count = 2; // 'count changed: 1 -> 2' + update DOM
uiState.message = 'Hello!'; // 'message changed: Welcome -> Hello!' + update DOM
// =====================================================
// Deep Reactive Object
// =====================================================
function createDeepReactive(obj, onChange, path = '') {
const handler = {
set(target, prop, value) {
const fullPath = path ? `${path}.${prop}` : prop;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
value = createDeepReactive(value, onChange, fullPath);
}
const oldValue = target[prop];
target[prop] = value;
onChange(fullPath, value, oldValue);
return true;
},
get(target, prop) {
return target[prop];
}
};
// Create deep reactive wrappers for nested objects
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
obj[key] = createDeepReactive(obj[key], onChange, path ? `${path}.${key}` : key);
}
}
return new Proxy(obj, handler);
}
// Usage
const studentForm = createDeepReactive(
{
personal: { name: '', email: '' },
academic: { majorCode: '', year: 2021 }
},
(path, value) => {
console.log(`Field ${path} updated:`, value);
updateSubmitButton();
}
);
studentForm.personal.name = 'Mike';
// Log: Field personal.name updated: Mike
Example: Permission Control Proxy
// =====================================================
// Role-Based Access Control
// =====================================================
function createSecureProxy(target, permissions) {
const currentUser = permissions.user;
const role = permissions.role;
const accessRules = {
admin: { read: '*', write: '*', delete: '*' },
lecturer: {
read: ['name', 'studentId', 'gpa', 'grade'],
write: ['grade'],
delete: []
},
student: {
read: ['name', 'studentId'],
write: [],
delete: []
},
guest: {
read: ['name'],
write: [],
delete: []
}
};
const rules = accessRules[role] || accessRules.guest;
return new Proxy(target, {
get(obj, prop) {
if (typeof prop === 'symbol') return Reflect.get(obj, prop);
const canRead = rules.read === '*' || rules.read.includes(prop);
if (!canRead) {
console.warn(`ACCESS DENIED: ${currentUser} (${role}) cannot read '${prop}'`);
return undefined;
}
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
const canWrite = rules.write === '*' || rules.write.includes(prop);
if (!canWrite) {
console.warn(`ACCESS DENIED: ${currentUser} (${role}) cannot write '${prop}'`);
return false;
}
console.log(`WRITE: ${currentUser} set ${prop} = ${value}`);
return Reflect.set(obj, prop, value);
},
deleteProperty(obj, prop) {
const canDelete = rules.delete === '*' || rules.delete.includes(prop);
if (!canDelete) {
console.warn(`ACCESS DENIED: ${currentUser} (${role}) cannot delete '${prop}'`);
return false;
}
return Reflect.deleteProperty(obj, prop);
}
});
}
// Usage
const studentRecord = {
name: 'Mike',
studentId: '1234567890',
gpa: 3.75,
password: 'hashed_password',
address: '1 Independence St'
};
// As a student
const studentView = createSecureProxy(studentRecord, {
user: 'Mike',
role: 'student'
});
console.log(studentView.name); // 'Mike' (allowed)
console.log(studentView.studentId); // '1234567890' (allowed)
console.log(studentView.gpa); // undefined (ACCESS DENIED)
console.log(studentView.password); // undefined (ACCESS DENIED)
// As a lecturer
const lecturerView = createSecureProxy(studentRecord, {
user: 'Dr. John',
role: 'lecturer'
});
console.log(lecturerView.name); // 'Mike'
console.log(lecturerView.gpa); // 3.75
lecturerView.grade = 90; // WRITE: Dr. John set grade = 90
// lecturerView.password = 'x'; // ACCESS DENIED
Code Explanation
Proxy Traps
get(target, prop, receiver)- Intercepts property readsset(target, prop, value, receiver)- Intercepts property writeshas(target, prop)- Intercepts theinoperatordeleteProperty(target, prop)- Interceptsdeleteapply(target, thisArg, args)- Intercepts function callsconstruct(target, args, newTarget)- InterceptsnewownKeys(target)- InterceptsObject.keys(), etc.getPrototypeOf(target)- InterceptsObject.getPrototypeOf()
Reflect Methods
Every Proxy trap has a corresponding Reflect method:
- Reflect.get() ↔ get trap
- Reflect.set() ↔ set trap
- Reflect.has() ↔ has trap
- Reflect.deleteProperty() ↔ deleteProperty trap
Revocable Proxy
Proxy.revocable(target, handler)returns{ proxy, revoke }- After
revoke(), all operations on the proxy will throw a TypeError - Useful for temporary access control
Conclusion
Proxy and Reflect are powerful metaprogramming tools in JavaScript. With Proxy, you can intercept and modify every operation on an object for validation, logging, access control, and reactive systems. For a campus application, Proxy is very useful for automatic form data validation, audit trails of student data access, role-based access control, and reactive UI state management. Always use Reflect methods when handling default operations to maintain correct behavior.