Skip to main content

JavaScript Meta Programming Guide

Table of Contentsโ€‹


Introductionโ€‹

The Proxy and Reflect objects allow you to intercept and define custom behavior for fundamental language operations (e.g., property lookup, assignment, enumeration, function invocation, etc.). With these two objects, you can program at the meta level of JavaScript.

Meta programming enables you to:

  • Intercept and customize fundamental operations
  • Create virtual properties
  • Implement validation logic
  • Add logging and debugging capabilities
  • Build reactive data systems

Proxiesโ€‹

What are Proxies?โ€‹

Proxy objects allow you to intercept certain operations and implement custom behaviors. A proxy wraps another object and intercepts operations like reading/writing properties, function calls, and more.

Basic Exampleโ€‹

Getting a property on an object:

const handler = {
get(target, name) {
return name in target ? target[name] : 42;
},
};

const p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, 42

In this example:

  • The Proxy object defines a target (an empty object) and a handler object
  • The handler implements a get trap
  • When accessing an undefined property, instead of returning undefined, it returns 42

Terminologyโ€‹

handlerโ€‹

A placeholder object which contains traps.

const handler = {
get(target, prop) { /* ... */ },
set(target, prop, value) { /* ... */ }
};

trapsโ€‹

The methods that provide property access. This is analogous to the concept of traps in operating systems.

// 'get' is a trap
get(target, property, receiver) {
// Custom behavior
}

targetโ€‹

The object which the proxy virtualizes. It is often used as storage backend for the proxy.

const target = { message: "Hello" };
const proxy = new Proxy(target, handler);

invariantsโ€‹

Semantics that remain unchanged when implementing custom operations. If you violate the invariants of a handler, a TypeError will be thrown.

Example invariant: A non-configurable property cannot be reported as non-existent by the get trap.


Handlers and Trapsโ€‹

Available Trapsโ€‹

The following table summarizes all available traps for Proxy objects:

Handler / TrapInterceptions
handler.getPrototypeOf()Object.getPrototypeOf()
Reflect.getPrototypeOf()
__proto__
Object.prototype.isPrototypeOf()
instanceof
handler.setPrototypeOf()Object.setPrototypeOf()
Reflect.setPrototypeOf()
handler.isExtensible()Object.isExtensible()
Reflect.isExtensible()
handler.preventExtensions()Object.preventExtensions()
Reflect.preventExtensions()
handler.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor()
handler.defineProperty()Object.defineProperty()
Reflect.defineProperty()
handler.has()Property query: foo in proxy
Inherited property query: foo in Object.create(proxy)
Reflect.has()
handler.get()Property access: proxy[foo], proxy.bar
Inherited property access: Object.create(proxy)[foo]
Reflect.get()
handler.set()Property assignment: proxy[foo] = bar, proxy.foo = bar
Inherited property assignment: Object.create(proxy)[foo] = bar
Reflect.set()
handler.deleteProperty()Property deletion: delete proxy[foo], delete proxy.foo
Reflect.deleteProperty()
handler.ownKeys()Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
handler.apply()proxy(..args)
Function.prototype.apply()
Function.prototype.call()
Reflect.apply()
handler.construct()new proxy(...args)
Reflect.construct()

Trap Examplesโ€‹

get trapโ€‹

const handler = {
get(target, prop, receiver) {
console.log(`Property ${prop} was accessed`);
return Reflect.get(target, prop, receiver);
}
};

const proxy = new Proxy({ name: "John" }, handler);
console.log(proxy.name); // Logs: "Property name was accessed", then "John"

set trapโ€‹

const handler = {
set(target, prop, value, receiver) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
return Reflect.set(target, prop, value, receiver);
}
};

const proxy = new Proxy({}, handler);
proxy.age = 25; // Works
proxy.age = "25"; // Throws TypeError

has trapโ€‹

const handler = {
has(target, prop) {
if (prop.startsWith('_')) {
return false; // Hide private properties
}
return Reflect.has(target, prop);
}
};

const proxy = new Proxy({ _secret: 'hidden', public: 'visible' }, handler);
console.log('public' in proxy); // true
console.log('_secret' in proxy); // false

deleteProperty trapโ€‹

const handler = {
deleteProperty(target, prop) {
if (prop.startsWith('_')) {
throw new Error('Cannot delete private properties');
}
return Reflect.deleteProperty(target, prop);
}
};

const proxy = new Proxy({ _id: 1, name: 'John' }, handler);
delete proxy.name; // Works
delete proxy._id; // Throws Error

apply trapโ€‹

const handler = {
apply(target, thisArg, argumentsList) {
console.log(`Function called with args: ${argumentsList}`);
return Reflect.apply(target, thisArg, argumentsList);
}
};

const sum = (a, b) => a + b;
const proxy = new Proxy(sum, handler);
proxy(2, 3); // Logs: "Function called with args: 2,3", Returns: 5

construct trapโ€‹

const handler = {
construct(target, args, newTarget) {
console.log(`Constructor called with args: ${args}`);
return Reflect.construct(target, args, newTarget);
}
};

class Person {
constructor(name) {
this.name = name;
}
}

const ProxyPerson = new Proxy(Person, handler);
const john = new ProxyPerson('John'); // Logs: "Constructor called with args: John"

Revocable Proxyโ€‹

The Proxy.revocable() method creates a revocable Proxy object. This means the proxy can be revoked via the revoke function, which switches the proxy off.

After revocation, any operation on the proxy leads to a TypeError.

const revocable = Proxy.revocable(
{},
{
get(target, name) {
return `[[${name}]]`;
},
},
);

const proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"

// Revoke the proxy
revocable.revoke();

console.log(proxy.foo); // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.foo = 1; // TypeError: Cannot perform 'set' on a proxy that has been revoked
delete proxy.foo; // TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked
console.log(typeof proxy); // "object" - typeof doesn't trigger any trap

Use cases for revocable proxies:

  • Temporary access control
  • Resource management
  • Security boundaries
  • Time-limited permissions

Reflectionโ€‹

What is Reflect?โ€‹

Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of the proxy handler's traps.

Key characteristics:

  • Reflect is not a function object (cannot be called or constructed)
  • It's a plain object containing static methods
  • Methods correspond one-to-one with proxy traps
  • Helps with forwarding default operations from handler to target

Reflect Methodsโ€‹

All Reflect methods correspond to proxy traps:

Reflect.get(target, propertyKey[, receiver])
Reflect.set(target, propertyKey, value[, receiver])
Reflect.has(target, propertyKey)
Reflect.deleteProperty(target, propertyKey)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, propertyKey)
Reflect.defineProperty(target, propertyKey, attributes)
Reflect.ownKeys(target)
Reflect.apply(target, thisArgument, argumentsList)
Reflect.construct(target, argumentsList[, newTarget])

Benefits of Reflectโ€‹

1. Functional approach to operatorsโ€‹

With Reflect.has(), you get the in operator as a function:

Reflect.has(Object, "assign"); // true

// Instead of:
"assign" in Object; // true

2. Better apply() functionโ€‹

Before Reflect:

Function.prototype.apply.call(Math.floor, undefined, [1.75]);

With Reflect:

Reflect.apply(Math.floor, undefined, [1.75]); // 1

Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hello"

Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index;
// 4

Reflect.apply("".charAt, "ponies", [3]);
// "i"

3. Boolean return values instead of exceptionsโ€‹

With Object.defineProperty():

try {
Object.defineProperty(obj, 'prop', { value: 42 });
// Success
} catch (e) {
// Failed
}

With Reflect.defineProperty():

if (Reflect.defineProperty(obj, 'prop', { value: 42 })) {
// Success
} else {
// Failed
}

4. Proper this binding in proxy trapsโ€‹

const handler = {
get(target, prop, receiver) {
// receiver ensures correct 'this' binding
return Reflect.get(target, prop, receiver);
}
};

Practical Use Casesโ€‹

Validationโ€‹

Validate object properties before setting them:

const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Age must be an integer');
}
if (value < 0 || value > 150) {
throw new RangeError('Age must be between 0 and 150');
}
}
return Reflect.set(target, prop, value);
}
};

const person = new Proxy({}, validator);
person.age = 25; // Works
person.age = -5; // Throws RangeError
person.age = "25"; // Throws TypeError

Default Valuesโ€‹

Provide default values for missing properties:

const withDefaults = (target, defaults) => {
return new Proxy(target, {
get(obj, prop) {
return prop in obj ? obj[prop] : defaults[prop];
}
});
};

const settings = withDefaults(
{ theme: 'dark' },
{ theme: 'light', language: 'en', fontSize: 14 }
);

console.log(settings.theme); // 'dark'
console.log(settings.language); // 'en'
console.log(settings.fontSize); // 14

Logging and Debuggingโ€‹

Track property access and modifications:

const createLogger = (target, name) => {
return new Proxy(target, {
get(obj, prop) {
console.log(`[${name}] Getting property "${prop}"`);
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
console.log(`[${name}] Setting property "${prop}" to "${value}"`);
return Reflect.set(obj, prop, value);
}
});
};

const user = createLogger({ name: 'John' }, 'User');
user.name; // Logs: [User] Getting property "name"
user.age = 30; // Logs: [User] Setting property "age" to "30"

Data Bindingโ€‹

Create reactive data with automatic updates:

const createReactive = (target, onChange) => {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
const result = Reflect.set(obj, prop, value);
if (oldValue !== value) {
onChange(prop, oldValue, value);
}
return result;
}
});
};

const state = createReactive(
{ count: 0 },
(prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
}
);

state.count = 1; // Logs: "count changed from 0 to 1"
state.count = 2; // Logs: "count changed from 1 to 2"

Negative Array Indicesโ€‹

Access arrays with negative indices (like Python):

const createNegativeArray = (arr) => {
return new Proxy(arr, {
get(target, prop) {
const index = Number(prop);
if (Number.isInteger(index) && index < 0) {
return target[target.length + index];
}
return Reflect.get(target, prop);
}
});
};

const arr = createNegativeArray([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4

Private Propertiesโ€‹

Hide properties that start with underscore:

const hidePrivate = (target) => {
return new Proxy(target, {
has(obj, prop) {
if (typeof prop === 'string' && prop.startsWith('_')) {
return false;
}
return Reflect.has(obj, prop);
},
ownKeys(obj) {
return Reflect.ownKeys(obj).filter(
key => typeof key !== 'string' || !key.startsWith('_')
);
},
get(obj, prop) {
if (typeof prop === 'string' && prop.startsWith('_')) {
throw new Error(`Cannot access private property: ${prop}`);
}
return Reflect.get(obj, prop);
}
});
};

const obj = hidePrivate({ _secret: 'hidden', public: 'visible' });
console.log('public' in obj); // true
console.log('_secret' in obj); // false
console.log(Object.keys(obj)); // ['public']

Best Practicesโ€‹

1. Always use Reflect in trapsโ€‹

Always use Reflect methods to perform the default operation:

// Good
const handler = {
get(target, prop, receiver) {
// Custom logic here
return Reflect.get(target, prop, receiver);
}
};

// Bad - manual implementation is error-prone
const handler = {
get(target, prop) {
return target[prop]; // Doesn't handle receiver correctly
}
};

2. Return appropriate valuesโ€‹

Ensure traps return the expected types:

// set trap must return boolean
set(target, prop, value) {
// ... validation logic
return Reflect.set(target, prop, value); // Returns boolean
}

// has trap must return boolean
has(target, prop) {
return Reflect.has(target, prop); // Returns boolean
}

3. Respect invariantsโ€‹

Don't violate JavaScript's invariants or you'll get TypeErrors:

// Bad - violates invariant
const handler = {
getPrototypeOf(target) {
return null; // If target is non-extensible, must return actual prototype
}
};

4. Performance considerationsโ€‹

Proxies add overhead. Use them judiciously:

// Don't wrap frequently accessed objects unnecessarily
// Only use proxies when you need interception

// Good - proxy for validation
const validatedUser = new Proxy(user, validator);

// Bad - unnecessary proxy
const simpleObject = new Proxy({}, {}); // Just use {}

5. Use revocable proxies for temporary accessโ€‹

const { proxy, revoke } = Proxy.revocable(sensitiveData, handler);

// Give temporary access
temporaryFunction(proxy);

// Revoke access when done
revoke();

6. Document proxy behaviorโ€‹

Make it clear when objects are proxies:

/**
* Creates a validated user object
* @returns {Proxy} A proxy that validates user properties
*/
function createUser(data) {
return new Proxy(data, validationHandler);
}

Additional Resourcesโ€‹