Proxy and Reflect

Proxy and Reflect are ES6 features for metaprogramming, which means writing code that manipulates other code. A Proxy lets you intercept and customize the fundamental operations on an object. Reflect gives you methods that perform those same operations.

Proxy Basics

A Proxy wraps an object and intercepts operations through “traps”:

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

const handler = {
  get(target, property) {
    console.log(`Getting ${property}`);
    return target[property];
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.name);
// Logs: "Getting name"
// Returns: "Alice"

Common Traps

Trap Intercepts
get Reading a property
set Writing a property
has The in operator
deleteProperty The delete operator
apply Function calls
construct The new operator
getPrototypeOf Object.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf()
ownKeys Object.keys(), Reflect.ownKeys()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor()
defineProperty Object.defineProperty()

Intercepting Property Reads with get

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

const proxy = new Proxy({ a: 1 }, handler);
console.log(proxy.a); // 1
console.log(proxy.b); // "Property "b" not found"

Intercepting Property Writes with set

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) {
      throw new RangeError("Age must be positive");
    }
    return Reflect.set(target, property, value, receiver);
  },
};

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

Intercepting the in Operator with has

const handler = {
  has(target, property) {
    if (typeof property === "string" && property.startsWith("_")) {
      return false; // Hide private properties
    }
    return Reflect.has(target, property);
  },
};

const obj = new Proxy({ _secret: "hidden", visible: "shown" }, handler);
console.log("visible" in obj); // true
console.log("_secret" in obj); // false

Intercepting Deletion with deleteProperty

const handler = {
  deleteProperty(target, property) {
    if (typeof property === "string" && property.startsWith("_")) {
      throw new Error("Cannot delete private properties");
    }
    return Reflect.deleteProperty(target, property);
  },
};

const obj = new Proxy({ _id: 1, name: "test" }, handler);
delete obj.name; // OK
// delete obj._id; // Error

Intercepting Function Calls with apply

function sum(a, b) {
  return a + b;
}

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

const proxy = new Proxy(sum, handler);
console.log(proxy(1, 2));
// Logs: "Called with args: 1,2"
// Returns: 3

Intercepting Construction with construct

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

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

const ProxiedPerson = new Proxy(Person, handler);
const p = new ProxiedPerson("Alice");
// Logs: "Creating instance with: Alice"