Steven's Knowledge
Advanced Features

Proxy & Reflect

Meta-programming with Proxy traps and Reflect API — reactive systems, validation, access control, and more

Proxy & Reflect

Proxy allows you to intercept and customize fundamental operations on objects (property access, assignment, function calls, etc.). Reflect provides methods that mirror these operations with consistent behavior.

Problems Solved

ProblemHow Proxy Solves It
No way to observe property changesset trap detects mutations
Manual validation on every assignmentProxy validates automatically
No access control on objectsget/set traps enforce permissions
Hard to build reactive UI systemsProxy enables fine-grained dependency tracking
Verbose logging/debugging of object accessTraps log operations transparently
Cannot intercept delete, in, iterationProxy traps cover all fundamental operations

How Proxy Works

                    ┌─────────────┐
   Code reads       │             │
   obj.name   ───►  │   Proxy     │  ───► get trap ───► return value
                    │  (Handler)  │
   Code writes      │             │
   obj.name = x ──► │             │  ───► set trap ───► validate & assign
                    └─────────────┘


                    ┌─────────────┐
                    │   Target    │  (Original object)
                    │   Object    │
                    └─────────────┘

Basic Usage

const target = { name: 'Alice', age: 30 };

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

  set(target, prop, value, receiver) {
    console.log(`Writing: ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

const proxy = new Proxy(target, handler);
proxy.name;        // logs "Reading: name" → "Alice"
proxy.age = 31;    // logs "Writing: age = 31"

Proxy Traps Reference

TrapInterceptsExample
get(target, prop, receiver)Property readobj.x, obj['x']
set(target, prop, value, receiver)Property writeobj.x = 1
has(target, prop)in operator'x' in obj
deleteProperty(target, prop)delete operatordelete obj.x
ownKeys(target)Key enumerationObject.keys(obj)
getOwnPropertyDescriptor(target, prop)Descriptor accessObject.getOwnPropertyDescriptor(obj, 'x')
defineProperty(target, prop, descriptor)Property definitionObject.defineProperty(obj, 'x', {...})
getPrototypeOf(target)Prototype accessObject.getPrototypeOf(obj)
setPrototypeOf(target, proto)Prototype mutationObject.setPrototypeOf(obj, proto)
isExtensible(target)Extensibility checkObject.isExtensible(obj)
preventExtensions(target)Prevent extensionsObject.preventExtensions(obj)
apply(target, thisArg, args)Function callfn() (function proxy)
construct(target, args, newTarget)new operatornew Fn() (function proxy)

Reflect API

Reflect provides a method for each Proxy trap, giving you a clean, consistent way to perform default operations:

// Without Reflect — inconsistent behavior
target[prop];                          // get
target[prop] = value;                  // set
delete target[prop];                   // delete
prop in target;                        // has
Object.keys(target);                   // ownKeys

// With Reflect — consistent, returns success/failure
Reflect.get(target, prop, receiver);           // → value
Reflect.set(target, prop, value, receiver);    // → boolean
Reflect.deleteProperty(target, prop);          // → boolean
Reflect.has(target, prop);                     // → boolean
Reflect.ownKeys(target);                       // → string[]

Why Reflect Matters in Proxy Handlers

const handler = {
  // BAD: bypasses prototype chain and receiver binding
  get(target, prop) {
    return target[prop];
  },

  // GOOD: preserves correct this binding and prototype chain
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
};

The receiver parameter ensures that this in getters refers to the proxy, not the raw target — critical for inheritance chains.

Real-World Patterns

Reactive System (Vue 3 Style)

// Simplified reactive system — core concept behind Vue 3 reactivity
let activeEffect = null;

function reactive(obj) {
  const deps = new Map(); // prop → Set<effect>

  return new Proxy(obj, {
    get(target, prop, receiver) {
      // Track: record which effect depends on this property
      if (activeEffect) {
        if (!deps.has(prop)) deps.set(prop, new Set());
        deps.get(prop).add(activeEffect);
      }
      return Reflect.get(target, prop, receiver);
    },

    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      // Trigger: re-run all effects that depend on this property
      if (deps.has(prop)) {
        deps.get(prop).forEach(effect => effect());
      }
      return result;
    }
  });
}

function watchEffect(fn) {
  activeEffect = fn;
  fn(); // Run once to collect dependencies
  activeEffect = null;
}

// Usage
const state = reactive({ count: 0, name: 'Alice' });

watchEffect(() => {
  console.log(`Count is: ${state.count}`);
});

state.count++;  // Automatically logs "Count is: 1"
state.count++;  // Automatically logs "Count is: 2"
state.name = 'Bob'; // No log — this effect doesn't depend on 'name'

Validation Layer

function validated(schema) {
  return new Proxy({}, {
    set(target, prop, value) {
      const validator = schema[prop];
      if (!validator) {
        throw new Error(`Unknown property: ${prop}`);
      }
      const error = validator(value);
      if (error) {
        throw new TypeError(`Invalid ${prop}: ${error}`);
      }
      return Reflect.set(target, prop, value);
    }
  });
}

const userSchema = {
  name: (v) => typeof v !== 'string' ? 'must be a string' : null,
  age: (v) => (!Number.isInteger(v) || v < 0 || v > 150) ? 'must be integer 0-150' : null,
  email: (v) => (typeof v !== 'string' || !v.includes('@')) ? 'must be valid email' : null,
};

const user = validated(userSchema);
user.name = 'Alice';        // OK
user.age = 30;              // OK
user.age = -5;              // TypeError: Invalid age: must be integer 0-150
user.unknown = 'x';         // Error: Unknown property: unknown

Immutable Objects

function immutable(obj) {
  return new Proxy(obj, {
    set() {
      throw new Error('Cannot modify immutable object');
    },
    deleteProperty() {
      throw new Error('Cannot delete from immutable object');
    },
    defineProperty() {
      throw new Error('Cannot define property on immutable object');
    }
  });
}

const config = immutable({ apiUrl: 'https://api.example.com', timeout: 5000 });
config.apiUrl;          // "https://api.example.com"
config.apiUrl = 'x';   // Error: Cannot modify immutable object

Negative Array Indices

function negativeArray(arr) {
  return new Proxy(arr, {
    get(target, prop, receiver) {
      const index = Number(prop);
      if (Number.isInteger(index) && index < 0) {
        return Reflect.get(target, target.length + index, receiver);
      }
      return Reflect.get(target, prop, receiver);
    }
  });
}

const arr = negativeArray([1, 2, 3, 4, 5]);
arr[-1];  // 5
arr[-2];  // 4

API Client with Auto-URL Building

function apiClient(baseUrl) {
  const buildPath = (segments) => `${baseUrl}/${segments.join('/')}`;

  function createProxy(segments = []) {
    return new Proxy(() => {}, {
      get(_, prop) {
        return createProxy([...segments, prop]);
      },
      apply(_, __, args) {
        const [options = {}] = args;
        const url = buildPath(segments);
        return fetch(url, {
          headers: { 'Content-Type': 'application/json' },
          ...options,
        }).then(r => r.json());
      }
    });
  }

  return createProxy();
}

const api = apiClient('https://api.example.com');
// These build URLs automatically:
api.users();                    // GET /users
api.users('123').posts();       // GET /users/123/posts
api.users('123').posts({ method: 'POST', body: JSON.stringify({...}) });

Access Control / Private Properties

function withAccessControl(obj, privatePrefix = '_') {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
        throw new Error(`Access denied: ${prop} is private`);
      }
      return Reflect.get(target, prop, receiver);
    },

    set(target, prop, value, receiver) {
      if (typeof prop === 'string' && prop.startsWith(privatePrefix)) {
        throw new Error(`Access denied: cannot set private ${prop}`);
      }
      return Reflect.set(target, prop, value, receiver);
    },

    ownKeys(target) {
      return Reflect.ownKeys(target).filter(
        key => typeof key !== 'string' || !key.startsWith(privatePrefix)
      );
    }
  });
}

const obj = withAccessControl({ name: 'Alice', _secret: '12345' });
obj.name;           // "Alice"
obj._secret;        // Error: Access denied: _secret is private
Object.keys(obj);   // ["name"] — _secret is hidden

Change Logging / Audit Trail

function audited(obj) {
  const log = [];

  const proxy = new Proxy(obj, {
    set(target, prop, value) {
      log.push({
        action: 'set',
        prop,
        oldValue: target[prop],
        newValue: value,
        timestamp: Date.now(),
      });
      return Reflect.set(target, prop, value);
    },

    deleteProperty(target, prop) {
      log.push({
        action: 'delete',
        prop,
        oldValue: target[prop],
        timestamp: Date.now(),
      });
      return Reflect.deleteProperty(target, prop);
    }
  });

  proxy.__audit = () => [...log];
  return proxy;
}

const user = audited({ name: 'Alice', role: 'viewer' });
user.role = 'admin';
user.name = 'Bob';
console.log(user.__audit());
// [{ action: 'set', prop: 'role', oldValue: 'viewer', newValue: 'admin', ... },
//  { action: 'set', prop: 'name', oldValue: 'Alice', newValue: 'Bob', ... }]

Proxy vs Other Approaches

ApproachProsCons
ProxyIntercepts all operations, transparentCannot proxy primitives, slight overhead
Object.definePropertyWide support, per-propertyCannot detect new/deleted properties
Getter/Setter methodsExplicit, no magicVerbose, requires discipline
Immutable libraries (Immer)Structural sharing, patchesDependency, different API

Performance Considerations

Operation Cost (relative):
Direct property access:     1x
Proxy get (pass-through):   ~3-5x
Proxy get (with logic):     ~5-10x
  • Avoid proxying hot-path objects accessed millions of times per second
  • Proxy overhead is negligible for typical application logic (state management, form validation, API clients)
  • Revocable proxies (Proxy.revocable()) let you disable a proxy when no longer needed
const { proxy, revoke } = Proxy.revocable(target, handler);
proxy.name;   // Works
revoke();
proxy.name;   // TypeError: Cannot perform 'get' on a proxy that has been revoked

Best Practices

  • Always use Reflect inside trap handlers to preserve correct behavior
  • Pass receiver to Reflect.get/Reflect.set for proper this binding
  • Return true from set traps (or use Reflect.set which does this)
  • Use Proxy.revocable() for temporary access patterns
  • Avoid deep nesting of proxies — proxy only the objects you need to observe
  • Proxy is not polyfillable — ensure your target environments support it

On this page