Skillversitas

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 reads
  • set(target, prop, value, receiver) - Intercepts property writes
  • has(target, prop) - Intercepts the in operator
  • deleteProperty(target, prop) - Intercepts delete
  • apply(target, thisArg, args) - Intercepts function calls
  • construct(target, args, newTarget) - Intercepts new
  • ownKeys(target) - Intercepts Object.keys(), etc.
  • getPrototypeOf(target) - Intercepts Object.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.