Docs

README

13.6 Proxy and Reflect

Overview

Proxy and Reflect are powerful metaprogramming features introduced in ES6 that allow you to intercept and customize fundamental operations on objects. Proxy creates a wrapper around an object to intercept operations, while Reflect provides methods that correspond to the Proxy traps.

Learning Objectives

  • Understand the Proxy API and its use cases
  • Master the Reflect API and its relationship to Proxy
  • Implement common patterns using Proxy (validation, logging, etc.)
  • Create reactive systems and observable objects
  • Understand performance implications

The Proxy Object

Basic Syntax

const proxy = new Proxy(target, handler);
  • target: The original object to wrap
  • handler: Object with trap methods that intercept operations

Simple Example

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

const handler = {
  get(target, property, receiver) {
    console.log(`Getting ${property}`);
    return target[property];
  },
  set(target, property, value, receiver) {
    console.log(`Setting ${property} to ${value}`);
    target[property] = value;
    return true;
  },
};

const proxy = new Proxy(target, handler);

proxy.name; // Logs: "Getting name", returns "John"
proxy.age = 31; // Logs: "Setting age to 31"

Proxy Traps

get() - Property Access

const handler = {
  get(target, property, receiver) {
    if (property in target) {
      return target[property];
    }
    return `Property ${property} not found`;
  },
};

const obj = new Proxy({ name: 'John' }, handler);
console.log(obj.name); // "John"
console.log(obj.missing); // "Property missing not found"

set() - Property Assignment

const handler = {
  set(target, property, value, receiver) {
    if (property === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    if (property === 'age' && (value < 0 || value > 150)) {
      throw new RangeError('Age must be between 0 and 150');
    }
    target[property] = value;
    return true; // Must return true for successful set
  },
};

const person = new Proxy({}, handler);
person.name = 'John'; // OK
person.age = 30; // OK
person.age = 'thirty'; // TypeError
person.age = -5; // RangeError

has() - The 'in' Operator

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

const obj = new Proxy({ name: 'John', _secret: 'hidden' }, handler);
console.log('name' in obj); // true
console.log('_secret' in obj); // false

deleteProperty() - Property Deletion

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

const obj = new Proxy({ name: 'John', _id: 123 }, handler);
delete obj.name; // OK
delete obj._id; // Error

apply() - Function Calls

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

const sum = new Proxy((a, b) => a + b, handler);
console.log(sum(1, 2)); // Logs call info, returns 3

construct() - new Operator

const handler = {
  construct(target, argumentsList, newTarget) {
    console.log(`Creating new instance with args: ${argumentsList}`);
    return new target(...argumentsList);
  },
};

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

const ProxiedPerson = new Proxy(Person, handler);
const john = new ProxiedPerson('John'); // Logs construction

ownKeys() - Object.keys, etc.

const handler = {
  ownKeys(target) {
    // Filter out private properties
    return Reflect.ownKeys(target).filter((key) => !key.startsWith('_'));
  },
};

const obj = new Proxy({ name: 'John', age: 30, _secret: 'hidden' }, handler);
console.log(Object.keys(obj)); // ['name', 'age']

getOwnPropertyDescriptor() - Property Descriptors

const handler = {
  getOwnPropertyDescriptor(target, property) {
    if (property.startsWith('_')) {
      return undefined; // Hide private properties
    }
    return Object.getOwnPropertyDescriptor(target, property);
  },
};

The Reflect API

Reflect provides methods corresponding to Proxy traps with consistent behavior:

// Instead of:
target[property];
// Use:
Reflect.get(target, property, receiver);

// Instead of:
target[property] = value;
// Use:
Reflect.set(target, property, value, receiver);

// Instead of:
delete target[property];
// Use:
Reflect.deleteProperty(target, property);

// Instead of:
property in target;
// Use:
Reflect.has(target, property);

// Instead of:
Object.keys(target);
// Use:
Reflect.ownKeys(target);

Why Use Reflect?

// 1. Consistent return values (boolean for success/failure)
const success = Reflect.set(target, 'name', 'John');

// 2. Proper receiver handling for inheritance
const handler = {
  get(target, property, receiver) {
    // receiver is the proxy, not target - important for inheritance
    return Reflect.get(target, property, receiver);
  },
};

// 3. Functional alternative to operators
Reflect.apply(fn, thisArg, args); // fn.apply(thisArg, args)
Reflect.construct(Class, args); // new Class(...args)

Practical Patterns

Validation Proxy

function createValidatedObject(schema) {
  return new Proxy(
    {},
    {
      set(target, property, value) {
        const validator = schema[property];
        if (validator && !validator(value)) {
          throw new TypeError(`Invalid value for ${property}`);
        }
        target[property] = value;
        return true;
      },
    }
  );
}

const user = createValidatedObject({
  name: (v) => typeof v === 'string' && v.length > 0,
  age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
  email: (v) => /^.+@.+\..+$/.test(v),
});

user.name = 'John'; // OK
user.age = 30; // OK
user.email = 'john@example.com'; // OK
user.age = -5; // TypeError

Observable Object

function createObservable(target, onChange) {
  return new Proxy(target, {
    set(target, property, value, receiver) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value, receiver);
      if (result && oldValue !== value) {
        onChange(property, oldValue, value);
      }
      return result;
    },
    deleteProperty(target, property) {
      const oldValue = target[property];
      const result = Reflect.deleteProperty(target, property);
      if (result) {
        onChange(property, oldValue, undefined);
      }
      return result;
    },
  });
}

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

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

Negative Array Indices

function createNegativeArray(arr) {
  return new Proxy(arr, {
    get(target, property, receiver) {
      const index = Number(property);
      if (!isNaN(index) && index < 0) {
        property = String(target.length + index);
      }
      return Reflect.get(target, property, receiver);
    },
  });
}

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

Auto-vivification

function createAutoVivify() {
  return new Proxy(
    {},
    {
      get(target, property) {
        if (!(property in target)) {
          target[property] = createAutoVivify();
        }
        return target[property];
      },
    }
  );
}

const data = createAutoVivify();
data.a.b.c.d = 'deep value';
console.log(data.a.b.c.d); // "deep value"

Logging Proxy

function createLoggingProxy(target, name = 'Object') {
  return new Proxy(target, {
    get(target, property, receiver) {
      console.log(`[${name}] GET ${String(property)}`);
      const value = Reflect.get(target, property, receiver);
      if (typeof value === 'object' && value !== null) {
        return createLoggingProxy(value, `${name}.${String(property)}`);
      }
      return value;
    },
    set(target, property, value, receiver) {
      console.log(
        `[${name}] SET ${String(property)} = ${JSON.stringify(value)}`
      );
      return Reflect.set(target, property, value, receiver);
    },
  });
}

const logged = createLoggingProxy({ user: { name: 'John' } });
logged.user.name = 'Jane';
// [Object] GET user
// [Object.user] SET name = "Jane"

Memoization Proxy

function memoize(fn) {
  const cache = new Map();
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        return cache.get(key);
      }
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    },
  });
}

const expensiveFn = memoize((n) => {
  console.log(`Computing for ${n}...`);
  return n * 2;
});

expensiveFn(5); // "Computing for 5...", 10
expensiveFn(5); // 10 (cached)

Revocable Proxy

const { proxy, revoke } = Proxy.revocable(
  { name: 'John' },
  {
    get(target, property) {
      return target[property];
    },
  }
);

console.log(proxy.name); // "John"
revoke();
console.log(proxy.name); // TypeError: Cannot perform 'get' on revoked proxy

Complete Trap Reference

TrapTriggered By
getproperty access
setproperty assignment
hasin operator
deletePropertydelete operator
ownKeysObject.keys(), for...in
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty()
getPrototypeOfObject.getPrototypeOf()
setPrototypeOfObject.setPrototypeOf()
isExtensibleObject.isExtensible()
preventExtensionsObject.preventExtensions()
applyfunction call
constructnew operator

Performance Considerations

  • Proxies add overhead to every intercepted operation
  • Use sparingly in performance-critical code
  • Consider alternatives for simple cases
  • Revocable proxies are slightly slower

Browser Support

Proxies are well-supported in modern browsers but cannot be polyfilled due to their fundamental nature.

Summary

ConceptPurpose
ProxyIntercept object operations
ReflectConsistent API for object operations
TrapsHandler methods for specific operations
Revocable ProxyProxy that can be disabled

Resources

README - JavaScript Tutorial | DeepML