Skip to content

Instantly share code, notes, and snippets.

@lifeart
Created February 20, 2025 19:24
Show Gist options
  • Save lifeart/d45cb73eb532c4bd04084443167f8451 to your computer and use it in GitHub Desktop.
Save lifeart/d45cb73eb532c4bd04084443167f8451 to your computer and use it in GitHub Desktop.
proxy.md

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:


Key Use Cases & Benefits

1. Dynamic Descriptor Attributes

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)

2. Validation & Security

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 });

3. Debugging & Logging

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

4. Default Values

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)

5. Conditional Getters/Setters

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

Caveats & Considerations

  • 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.

When to Use This Pattern

  • 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment