Using a Proxy as the property descriptor itself in JavaScript allows you to intercept and customize how the engine interacts with the descriptor's attributes (like value
, get
, set
, writable
, etc.) during property definition. This approach provides dynamic control over the descriptor's behavior at the moment the property is defined. Here's how you can leverage this technique and its benefits:
Intercept access to descriptor properties (e.g., value
, get
, set
) and compute them on-the-fly during property definition.
const dynamicDescriptor = new Proxy({}, {
get(target, prop) {
if (prop === 'value') {
return Math.random(); // Return a random value each time
}
return Reflect.get(target, prop);
},
});
const obj = {};
Object.defineProperty(obj, 'random', {
enumerable: true,
configurable: true,
...dynamicDescriptor, // Proxy acts as the descriptor
});
console.log(obj.random); // Random value (e.g., 0.123)
console.log(obj.random); // Same value (fixed at definition time)
Enforce rules when defining properties, such as blocking invalid configurations.
const validatorProxy = new Proxy({}, {
get(target, prop) {
if (prop === 'writable' && target[prop] === undefined) {
return false; // Force writable to be false if not specified
}
return Reflect.get(target, prop);
},
set(target, prop, value) {
if (prop === 'enumerable' && value === true) {
throw new Error("Properties cannot be enumerable!");
}
return Reflect.set(target, prop, value);
},
});
const obj = {};
Object.defineProperty(obj, 'secret', {
configurable: true,
...validatorProxy, // Proxy enforces rules
value: 42,
});
// Throws Error: "Properties cannot be enumerable!"
Object.defineProperty(obj, 'secret', { enumerable: true });
Track how the engine interacts with the descriptor during property definition.
const loggingProxy = new Proxy({}, {
get(target, prop) {
console.log(`Descriptor property accessed: ${prop}`);
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`Descriptor property set: ${prop} = ${value}`);
return Reflect.set(target, prop, value);
},
});
const obj = {};
Object.defineProperty(obj, 'test', {
...loggingProxy, // Proxy logs descriptor interactions
value: 'hello',
});
// Logs:
// Descriptor property accessed: value
// Descriptor property set: value = hello
Provide fallbacks for missing descriptor attributes.
const defaultProxy = new Proxy({}, {
get(target, prop) {
// Default to non-enumerable and non-writable
if (prop === 'enumerable') return false;
if (prop === 'writable') return false;
return Reflect.get(target, prop);
},
});
const obj = {};
Object.defineProperty(obj, 'id', {
...defaultProxy, // Proxy fills in defaults
value: 123,
});
console.log(Object.keys(obj)); // [] (non-enumerable)
obj.id = 456; // Fails silently (non-writable)
Generate getter/setter functions dynamically based on context.
const getterSetterProxy = new Proxy({}, {
get(target, prop) {
if (prop === 'get') {
return () => {
console.log('Property accessed!');
return target._value;
};
}
if (prop === 'set') {
return (newValue) => {
console.log('Property updated!');
target._value = newValue;
};
}
return Reflect.get(target, prop);
},
});
const obj = {};
Object.defineProperty(obj, 'data', {
configurable: true,
...getterSetterProxy, // Proxy defines getter/setter
});
obj.data = 42; // Logs: "Property updated!"
console.log(obj.data); // Logs: "Property accessed!" → 42
- One-Time Interaction: The Proxy is only used during the
Object.defineProperty
call. Subsequent interactions with the property (e.g.,obj.prop = 1
) are controlled by the property’s actual descriptor, not the Proxy. - Attribute Requirements: The Proxy must provide valid attributes (e.g.,
configurable
,enumerable
) to avoid errors during property definition. - Performance: Proxies add overhead. Use sparingly in performance-critical code.
- Advanced Metadata Control: When you need dynamic or conditional property behavior at definition time.
- Framework/API Design: To enforce conventions or inject logic during property setup.
- Debugging: To trace how descriptors are processed internally.
By using a Proxy as the descriptor itself, you unlock powerful metaprogramming capabilities, enabling patterns that would otherwise require complex boilerplate or static configurations.