JavaScript Interview Prep
ES6+ Features

Proxy & Reflect

Intercept Every Operation on an Object

LinkedIn Hook

What if you could sit between every property read, every property write, every delete, every in check — and decide what happens?

That is what a Proxy does. It wraps an object and installs "traps" that fire on fundamental operations. Vue.js 3's reactivity. MobX observables. Negative array indexing. Validation schemas. Immutable wrappers. All built on the same 13-trap API.

And Reflect is the twin you call from inside those traps — the built-in object that gives you the default behavior for each operation. Instead of return target[prop] (which quietly breaks prototype chains), you return Reflect.get(target, prop, receiver) and everything just works.

Most developers never touch Proxy. Library authors never stop touching it. If you have ever wondered how frameworks know a property changed without you calling a setter — this lesson is the answer.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #Proxy #Reflect #ES6 #Metaprogramming #AdvancedJS


Proxy & Reflect thumbnail


What You'll Learn

  • What a Proxy is and the full list of 13 traps it can install
  • How Reflect mirrors every trap and why you should call it inside handlers
  • Real-world patterns: validation, reactive state, negative indexing, logging, revocable access

The Security Checkpoint Analogy

A Proxy is like a security checkpoint at a building entrance. Every person (operation) trying to enter or leave must pass through the checkpoint, where guards (traps) can inspect, modify, or deny access. Reflect is the rule book that tells guards the "normal" behavior for each operation.

Proxy Basics

const target = { name: "Rakibul", age: 25 };

const handler = {
  get(target, property, receiver) {
    console.log(`Reading '${property}'`);
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`Setting '${property}' to ${value}`);
    return Reflect.set(target, property, value, receiver);
  }
};

const proxy = new Proxy(target, handler);
proxy.name;       // Logs: Reading 'name' -> "Rakibul"
proxy.age = 26;   // Logs: Setting 'age' to 26

Validation Proxy

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

const user = createValidated({
  name: v => typeof v === "string" && v.length > 0,
  age: v => typeof v === "number" && v >= 0 && v <= 150,
  email: v => typeof v === "string" && v.includes("@")
});

user.name = "Rakibul"; // works
user.age = 25;         // works
// user.age = -5;      // TypeError: Invalid value for age: -5
// user.foo = "bar";   // Error: Unknown property: foo

Negative Array Index with Proxy

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

const arr = negativeArray([10, 20, 30, 40, 50]);
arr[0];   // 10
arr[-1];  // 50 (last element)
arr[-2];  // 40 (second to last)
arr.length; // 5 (non-numeric properties still work)

Reactive Data Binding (Vue.js 3 Pattern)

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

const state = reactive({ count: 0, name: "Rakibul" }, (prop, newVal, oldVal) => {
  console.log(`${prop} changed: ${oldVal} -> ${newVal}`);
  // In a real framework, this would trigger a re-render
});

state.count = 1;      // "count changed: 0 -> 1"
state.name = "Karim"; // "name changed: Rakibul -> Karim"
delete state.name;    // "name changed: Karim -> undefined"

Logging Proxy

function withLogging(obj) {
  return new Proxy(obj, {
    get(target, property, receiver) {
      console.log(`[GET] ${String(property)}`);
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      console.log(`[SET] ${String(property)} = ${JSON.stringify(value)}`);
      return Reflect.set(target, property, value, receiver);
    },
    has(target, property) {
      console.log(`[HAS] ${String(property)}`);
      return Reflect.has(target, property);
    },
    deleteProperty(target, property) {
      console.log(`[DELETE] ${String(property)}`);
      return Reflect.deleteProperty(target, property);
    }
  });
}

const obj = withLogging({ x: 1, y: 2 });
obj.x;          // [GET] x -> 1
obj.z = 3;      // [SET] z = 3
"x" in obj;     // [HAS] x -> true
delete obj.y;   // [DELETE] y

Proxy Traps — Complete List

const handler = {
  // Property access
  get(target, property, receiver) {},
  set(target, property, value, receiver) {},
  has(target, property) {},                // 'in' operator
  deleteProperty(target, property) {},     // 'delete' operator

  // Function call
  apply(target, thisArg, args) {},         // function call
  construct(target, args, newTarget) {},   // 'new' operator

  // Property definition
  getOwnPropertyDescriptor(target, property) {},
  defineProperty(target, property, descriptor) {},

  // Prototype
  getPrototypeOf(target) {},
  setPrototypeOf(target, prototype) {},

  // Enumeration
  ownKeys(target) {},                      // Object.keys, for...in

  // Extensibility
  isExtensible(target) {},
  preventExtensions(target) {}
};

Reflect — The Mirror of Proxy

Every Proxy trap has a corresponding Reflect method. Reflect provides the default behavior for each operation:

// Reflect.get — like obj[property]
Reflect.get({ a: 1 }, "a"); // 1

// Reflect.set — like obj[property] = value
const obj = {};
Reflect.set(obj, "a", 1); // true, obj.a === 1

// Reflect.has — like property in obj
Reflect.has({ a: 1 }, "a"); // true

// Reflect.deleteProperty — like delete obj[property]
Reflect.deleteProperty({ a: 1 }, "a"); // true

// Reflect.apply — like fn.apply(thisArg, args)
Reflect.apply(Math.max, null, [1, 2, 3]); // 3

// Reflect.construct — like new Constructor(...args)
Reflect.construct(Date, [2025, 0, 1]); // Date object

// Why use Reflect over direct operations?
// 1. Returns boolean instead of throwing (set, defineProperty)
// 2. Consistent functional interface
// 3. Proper forwarding in Proxy traps (preserves receiver)

Revocable Proxies

// Create a proxy that can be permanently disabled
const { proxy, revoke } = Proxy.revocable({ secret: "data" }, {
  get(target, property) {
    return Reflect.get(target, property);
  }
});

proxy.secret; // "data"

revoke(); // Permanently disable the proxy

// proxy.secret; // TypeError: Cannot perform 'get' on a proxy that has been revoked
// Useful for: temporary access tokens, permission revocation, API timeouts

Proxy & Reflect visual 1


Common Mistakes

  • Returning target[property] directly inside a get trap instead of Reflect.get(target, property, receiver) — breaks the prototype chain because the receiver is not forwarded. Always use Reflect with the receiver argument when forwarding.
  • Forgetting that set and deleteProperty traps must return a boolean — returning undefined in strict mode throws a TypeError. Always return Reflect.set(...) or explicit true.
  • Proxying Date, Map, or Set with only get/set traps — these built-ins store internal slots and do not call traps for their native methods. You must explicitly handle method calls via apply or bind them to the target.

Interview Questions

Q: What is a Proxy and how does it work?

A Proxy wraps a target object and intercepts fundamental operations (get, set, has, delete, etc.) via handler functions called "traps." The Proxy constructor takes a target and a handler object. Each trap receives the target and operation details, and can modify, validate, or log the operation before forwarding it.

Q: What is Reflect and why is it used with Proxy?

Reflect is a built-in object providing methods that mirror every Proxy trap. Using Reflect.get(), Reflect.set(), etc., inside traps ensures you perform the default operation correctly — especially important for maintaining the receiver (for proper prototype chain behavior). Reflect methods also return booleans instead of throwing on failure.

Q: Give 3 practical use cases for Proxy.

  1. Validation — intercept set to validate values before storing. 2) Reactive data — intercept set to trigger UI re-renders when data changes (Vue.js 3 uses this). 3) Negative array indexing — intercept get to support arr[-1] for last element access. Others: logging, access control, lazy loading, default values.

Q: What is a revocable proxy?

Proxy.revocable(target, handler) returns { proxy, revoke }. Calling revoke() permanently disables the proxy — any operation on it throws a TypeError. Useful for temporary access, permission windows, and secure API designs where access can be revoked.

Q: Name 4 Proxy traps.

get (property read), set (property write), has (in operator), deleteProperty (delete operator). Others include apply (function call), construct (new), ownKeys (Object.keys / for...in), and getPrototypeOf.

Q: How does Vue.js 3 use Proxy for reactivity?

Vue 3 wraps each reactive object in a Proxy. The get trap registers which effect (component render) accessed which property. The set trap then re-runs every effect that registered for that property. This replaces Vue 2's Object.defineProperty approach, which could not track newly added properties or array index assignments.

Q: How would you implement negative array indexing?

Wrap the array in a Proxy with a get trap: if the property is a negative integer, return target[target.length + index]; otherwise forward via Reflect.get. See the negativeArray example above.


Quick Reference — Cheat Sheet

PROXY & REFLECT
  new Proxy(target, handler)      -> wrap target with traps
  Proxy.revocable(target, handler) -> { proxy, revoke }

HANDLER TRAPS (13 total)
  get, set, has, deleteProperty    -> property access
  apply, construct                 -> function/new
  ownKeys                          -> enumeration
  getPrototypeOf, setPrototypeOf   -> prototype
  getOwnPropertyDescriptor         -> inspection
  defineProperty                   -> definition
  isExtensible, preventExtensions  -> extensibility

REFLECT — MIRROR OF PROXY
  Reflect.get / set / has / deleteProperty
  Reflect.apply / construct
  Reflect.ownKeys / getPrototypeOf / setPrototypeOf
  Always forward `receiver` when calling Reflect from a trap

WHY REFLECT OVER DIRECT OPS
  1. Returns boolean instead of throwing
  2. Consistent functional interface
  3. Preserves prototype chain via receiver

USE CASES
  Validation | Reactive state (Vue 3) | Logging | Negative indexing
  Lazy loading | Default values | Access control | API revocation

Previous: Lesson 9.7 — Set, Map, WeakSet, WeakMap Next: Lesson 10.1 — try...catch...finally


This is Lesson 9.8 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page