| | |

JavaScript 58 🧬 Proxy and Reflect

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

const proxy = new Proxy(target, {
  get(obj, prop) {
    console.log(`Getting ${prop}`);
    return obj[prop];
  },
  set(obj, prop, value) {
    console.log(`Setting ${prop} to ${value}`);
    obj[prop] = value;
    return true;
  }
});

console.log(proxy.name);
proxy.age = 31;
console.log(target.age);

const handler = {
  has(obj, prop) {
    return prop in obj;
  },
  deleteProperty(obj, prop) {
    delete obj[prop];
    return true;
  }
};

const proxy2 = new Proxy(target, handler);
console.log('name' in proxy2);
delete proxy2.age;
console.log('age' in proxy2);

const validator = new Proxy({}, {
  set(obj, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    obj[prop] = value;
    return true;
  }
});

validator.age = 30;
console.log(validator.age);

const defaults = new Proxy({}, {
  get(obj, prop) {
    return obj[prop] ?? `default-${prop}`;
  }
});

console.log(defaults.name);
console.log(defaults.foo);

const reflectTest = {
  name: 'Alice'
};

console.log(Reflect.get(reflectTest, 'name'));
console.log(Reflect.has(reflectTest, 'name'));
Reflect.set(reflectTest, 'age', 30);
console.log(reflectTest.age);
console.log(Reflect.ownKeys(reflectTest));

A Proxy wraps an object and intercepts its operations — reads, writes, deletes, function calls, and more. A Reflect object provides the default behaviors that a Proxy can use — the same operations, but with a clean API. Together they’re the foundation of JavaScript metaprogramming.

Key point: A Proxy is a trap-based interceptor. It doesn’t change the target object — it wraps it. Every operation goes through a handler function you define. Reflect is the toolbox of default behaviors you’d otherwise implement by hand.


a – What is a Proxy

A Proxy creates a transparent wrapper around an object. It intercepts fundamental operations and lets you customize what they do.

Basic syntax:

const proxy = new Proxy(target, handler);
  • target — the object being wrapped
  • handler — an object with traps (interceptor functions)

The simplest proxy:

const target = { name: 'Alice' };
const proxy = new Proxy(target, {});

console.log(proxy.name);
// [ 'Alice' ]

With an empty handler, the proxy behaves exactly like the target.

Why Proxies matter:

  • Intercept any operation on an object
  • Validate writes before they happen
  • Provide defaults for missing properties
  • Log or debug access patterns
  • Implement lazy loading, caching, immutability
  • Power frameworks like Vue 3 and MobX

Proxy vs Object.defineProperty:

Object.defineProperty intercepts a single property. Proxy intercepts everything — all properties, all operations.

FeaturedefinePropertyProxy
Per-property✅ (via traps)
Whole object
Method calls
in operator
delete
ownKeys

The available traps:

TrapIntercepts
getProperty read
setProperty write
hasin operator
deletePropertydelete
ownKeysObject.keys, getOwnPropertyNames
getOwnPropertyDescriptorDescriptor lookup
definePropertyObject.defineProperty
getPrototypeOfPrototype read
setPrototypeOfPrototype write
isExtensibleExtensibility check
preventExtensionsMake non-extensible
applyFunction call
constructnew operator

The get trap:

const proxy = new Proxy({ name: 'Alice' }, {
  get(target, prop) {
    console.log(`Reading ${prop}`);
    return target[prop];
  }
});

console.log(proxy.name);
// [ Reading name ]
// [ 'Alice' ]

The set trap:

const proxy = new Proxy({}, {
  set(target, prop, value) {
    console.log(`Writing ${prop} = ${value}`);
    target[prop] = value;
    return true;
  }
});

proxy.age = 30;
// [ Writing age = 30 ]

The trap must return true for the write to succeed. Returning false throws a TypeError in strict mode.

The has trap — the in operator:

const proxy = new Proxy({ name: 'Alice' }, {
  has(target, prop) {
    return prop in target;
  }
});

console.log('name' in proxy);
// [ true ]

console.log('age' in proxy);
// [ false ]

The deleteProperty trap:

const proxy = new Proxy({ name: 'Alice' }, {
  deleteProperty(target, prop) {
    console.log(`Deleting ${prop}`);
    delete target[prop];
    return true;
  }
});

delete proxy.name;
// [ Deleting name ]

The ownKeys trap:

const proxy = new Proxy({ name: 'Alice', age: 30 }, {
  ownKeys(target) {
    return Object.keys(target);
  }
});

console.log(Object.keys(proxy));
// [ [ 'name', 'age' ] ]

Why the target and proxy stay in sync:

Operations on the proxy forward to the target by default. If a trap doesn’t do anything, the target is unchanged.

const target = { a: 1 };
const proxy = new Proxy(target, {});

proxy.b = 2;
console.log(target.b);
// [ 2 ]

When to use Proxies:

Use caseExample
ValidationCheck writes before they happen
DefaultsFill in missing values
LoggingTrace reads and writes
CachingMemoize property access
Access controlBlock certain keys
ImmutabilityBlock all writes
Lazy loadingFetch on access
ObservablesReact to changes

Basic example — read/write logging:

function createLogger(target) {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`get ${String(prop)}`);
      return Reflect.get(obj, prop);
    },
    set(obj, prop, value) {
      console.log(`set ${String(prop)} = ${value}`);
      return Reflect.set(obj, prop, value);
    }
  });
}

const logged = createLogger({ name: 'Alice' });

logged.name;
// [ get name ]

logged.age = 30;
// [ set age = 30 ]

b – The Proxy traps

Each trap intercepts a specific operation. Here are the important ones.

get(target, prop, receiver) — reading:

const proxy = new Proxy({ name: 'Alice' }, {
  get(target, prop, receiver) {
    if (typeof prop === 'string' && !(prop in target)) {
      return `missing: ${prop}`;
    }
    return Reflect.get(target, prop, receiver);
  }
});

console.log(proxy.name);
// [ 'Alice' ]

console.log(proxy.missing);
// [ 'missing: missing' ]

The receiver is usually the proxy itself — useful for inherited getters.

set(target, prop, value, receiver) — writing:

const proxy = new Proxy({}, {
  set(target, prop, value, receiver) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('age must be a number');
    }
    return Reflect.set(target, prop, value, receiver);
  }
});

proxy.age = 30;        // ✅
proxy.age = 'thirty';  // ❌ TypeError

The trap returns true to allow, false to reject (strict mode throws).

has(target, prop)in operator:

const hidden = new Set(['secret']);
const proxy = new Proxy({ public: 1, secret: 2 }, {
  has(target, prop) {
    if (hidden.has(prop)) return false;
    return Reflect.has(target, prop);
  }
});

console.log('public' in proxy);
// [ true ]

console.log('secret' in proxy);
// [ false ]  ← hidden

deleteProperty(target, prop)delete:

const locked = new Set(['id']);
const proxy = new Proxy({ id: 1, name: 'Alice' }, {
  deleteProperty(target, prop) {
    if (locked.has(prop)) {
      throw new Error(`Cannot delete ${prop}`);
    }
    return Reflect.deleteProperty(target, prop);
  }
});

delete proxy.name;   // ✅
delete proxy.id;     // ❌ Error

ownKeys(target) — key enumeration:

const proxy = new Proxy({ a: 1, b: 2, _c: 3 }, {
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(k => !String(k).startsWith('_'));
  }
});

console.log(Object.keys(proxy));
// [ [ 'a', 'b' ] ]

getOwnPropertyDescriptor — descriptor lookup:

const proxy = new Proxy({ a: 1 }, {
  getOwnPropertyDescriptor(target, prop) {
    const desc = Reflect.getOwnPropertyDescriptor(target, prop);
    if (desc) desc.writable = false;
    return desc;
  }
});

defineProperty — descriptor setting:

const proxy = new Proxy({}, {
  defineProperty(target, prop, descriptor) {
    console.log(`Defining ${String(prop)}`);
    return Reflect.defineProperty(target, prop, descriptor);
  }
});

Object.defineProperty(proxy, 'x', { value: 42 });
// [ Defining x ]

apply(target, thisArg, args) — function call:

For a proxy around a function:

const fn = (a, b) => a + b;

const proxy = new Proxy(fn, {
  apply(target, thisArg, args) {
    console.log(`Called with ${args}`);
    return Reflect.apply(target, thisArg, args);
  }
});

console.log(proxy(2, 3));
// [ Called with 2,3 ]
// [ 5 ]

construct(target, args, newTarget)new:

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

const proxy = new Proxy(Person, {
  construct(target, args, newTarget) {
    console.log(`Constructing with ${args}`);
    return Reflect.construct(target, args, newTarget);
  }
});

const p = new proxy('Alice');
// [ Constructing with Alice ]

console.log(p.name);
// [ 'Alice' ]

getPrototypeOf and setPrototypeOf:

const proto = { greet() { return 'hi'; } };
const proxy = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});

console.log(proxy.greet());
// [ 'hi' ]

isExtensible and preventExtensions:

let locked = false;
const proxy = new Proxy({}, {
  isExtensible(target) {
    return Reflect.isExtensible(target) && !locked;
  },
  preventExtensions(target) {
    locked = true;
    return Reflect.preventExtensions(target);
  }
});

get for defaults — a common pattern:

const withDefaults = new Proxy({}, {
  get(target, prop) {
    if (prop in target) return target[prop];
    return `default-${String(prop)}`;
  }
});

console.log(withDefaults.name);
// [ 'default-name' ]

console.log(withDefaults.foo);
// [ 'default-foo' ]

set for validation — a common pattern:

const strict = new Proxy({}, {
  set(target, prop, value) {
    if (typeof prop === 'string' && prop.startsWith('_')) {
      throw new Error('Private props cannot be set directly');
    }
    return Reflect.set(target, prop, value);
  }
});

get for observability — track reads:

function observe(obj, onChange) {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      onChange('get', prop);
      return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
      onChange('set', prop, value);
      return Reflect.set(target, prop, value, receiver);
    }
  });
}

const state = observe({ count: 0 }, (op, prop, value) => {
  console.log(`${op}: ${String(prop)}${value ? ' = ' + value : ''}`);
});

state.count;
// [ get: count ]

state.count = 1;
// [ set: count = 1 ]

ownKeys for private keys — hide from enumeration:

const proxy = new Proxy({ a: 1, b: 2, secret: 3 }, {
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(k => k !== 'secret');
  },
  getOwnPropertyDescriptor(target, prop) {
    if (prop === 'secret') return undefined;
    return Reflect.getOwnPropertyDescriptor(target, prop);
  }
});

console.log(Object.keys(proxy));
// [ [ 'a', 'b' ] ]

All traps and their defaults:

TrapDefault behavior
getReflect.get(target, prop, receiver)
setReflect.set(target, prop, value, receiver)
hasReflect.has(target, prop)
deletePropertyReflect.deleteProperty(target, prop)
ownKeysReflect.ownKeys(target)
getOwnPropertyDescriptorReflect.getOwnPropertyDescriptor(target, prop)
definePropertyReflect.defineProperty(target, prop, desc)
getPrototypeOfReflect.getPrototypeOf(target)
setPrototypeOfReflect.setPrototypeOf(target, proto)
isExtensibleReflect.isExtensible(target)
preventExtensionsReflect.preventExtensions(target)
applyReflect.apply(target, thisArg, args)
constructReflect.construct(target, args, newTarget)

The Reflect API:

Reflect provides functions that mirror every proxy trap. They give the default behavior that proxies often forward to.

Reflect methods:

MethodPurpose
Reflect.get(obj, prop)Read property
Reflect.set(obj, prop, value)Write property
Reflect.has(obj, prop)in operator
Reflect.deleteProperty(obj, prop)delete
Reflect.ownKeys(obj)All keys
Reflect.getOwnPropertyDescriptor(obj, prop)Get descriptor
Reflect.defineProperty(obj, prop, desc)Define
Reflect.getPrototypeOf(obj)Get prototype
Reflect.setPrototypeOf(obj, proto)Set prototype
Reflect.isExtensible(obj)Can extend?
Reflect.preventExtensions(obj)Lock
Reflect.apply(fn, thisArg, args)Call function
Reflect.construct(Ctor, args)new

Why Reflect exists:

Before Reflect, some operations didn’t have a clean function form:

// Old way to call a function
fn.apply(thisArg, args);

// Old way to get a prototype
Object.getPrototypeOf(obj);

// Old way to check "in"
'key' in obj;

// Old way to delete
delete obj.key;

Reflect provides consistent functions that match the proxy traps:

Reflect.apply(fn, thisArg, args);
Reflect.getPrototypeOf(obj);
Reflect.has(obj, 'key');
Reflect.deleteProperty(obj, 'key');

Reflect preserves the receiver:

The receiver argument in Reflect.get matters when the target has getters. It ensures this inside the getter points to the proxy, not the target:

const base = {
  get name() { return this._name; },
  _name: 'Alice'
};

const proxy = new Proxy(base, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);  // ✅ passes receiver
  }
});

console.log(proxy.name);
// [ 'Alice' ]

Without the receiver, this._name would look for _name on target, not proxy — but in this case both are fine.

When to use Reflect:

  • Inside a proxy trap — to forward to the default behavior
  • When you need a function form of an operation
  • When you want consistent return values

When NOT to use Reflect:

  • Simple operations have cleaner syntax (in, delete, etc.)
  • For most code, obj.prop and obj.prop = value are clearer

Example — validating writes with Reflect:

const schema = {
  age: v => typeof v === 'number' && v >= 0,
  name: v => typeof v === 'string' && v.length > 0
};

const proxy = new Proxy({}, {
  set(target, prop, value, receiver) {
    const validator = schema[prop];
    if (validator && !validator(value)) {
      throw new TypeError(`Invalid value for ${prop}: ${value}`);
    }
    return Reflect.set(target, prop, value, receiver);
  }
});

proxy.name = 'Alice';   // ✅
proxy.age = 30;         // ✅

try {
  proxy.age = -1;
} catch (err) {
  console.log(err.message);
  // [ Invalid value for age: -1 ]
}

Example — read-only proxy:

function readOnly(target) {
  return new Proxy(target, {
    set() {
      throw new Error('Cannot modify read-only object');
    },
    deleteProperty() {
      throw new Error('Cannot delete from read-only object');
    },
    defineProperty() {
      throw new Error('Cannot define on read-only object');
    }
  });
}

const ro = readOnly({ name: 'Alice' });

console.log(ro.name);
// [ 'Alice' ]

try {
  ro.name = 'Bob';
} catch (err) {
  console.log(err.message);
  // [ Cannot modify read-only object ]
}

Example — negative array indices:

function withNegativeIndices(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
        const idx = target.length + Number(prop);
        return target[idx];
      }
      return Reflect.get(target, prop);
    },
    set(target, prop, value) {
      if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
        const idx = target.length + Number(prop);
        target[idx] = value;
        return true;
      }
      return Reflect.set(target, prop, value);
    }
  });
}

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

arr[-1] = 99;
console.log(arr[2]);
// [ 99 ]

Example — observable state (Vue-style):

function reactive(obj, onChange) {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      if (typeof value === 'object' && value !== null) {
        return reactive(value, onChange);
      }
      return value;
    },
    set(target, prop, value, receiver) {
      const oldValue = target[prop];
      const result = Reflect.set(target, prop, value, receiver);
      if (oldValue !== value) {
        onChange(prop, value, oldValue);
      }
      return result;
    }
  });
}

const state = reactive({ count: 0 }, (prop, val, old) => {
  console.log(`${String(prop)}: ${old} → ${val}`);
});

state.count = 1;
// [ count: 0 → 1 ]

state.count = 2;
// [ count: 1 → 2 ]

c – Common Proxy and Reflect patterns

These patterns come up constantly in real applications and libraries.

Pattern 1 — Default values:

function withDefaults(target, defaults) {
  return new Proxy(target, {
    get(obj, prop) {
      return prop in obj ? obj[prop] : defaults[prop];
    }
  });
}

const config = withDefaults({ port: 3000 }, { host: 'localhost' });
console.log(config.port);   // 3000
console.log(config.host);   // localhost

Pattern 2 — Validation:

function validate(target, rules) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const rule = rules[prop];
      if (rule && !rule(value)) {
        throw new TypeError(`Invalid ${String(prop)}`);
      }
      return Reflect.set(obj, prop, value);
    }
  });
}

const user = validate({}, {
  age: v => typeof v === 'number' && v >= 0,
  email: v => /@/.test(v)
});

Pattern 3 — Logging:

function log(target, label = 'obj') {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`${label}.${String(prop)}`);
      return Reflect.get(obj, prop);
    },
    set(obj, prop, value) {
      console.log(`${label}.${String(prop)} = ${value}`);
      return Reflect.set(obj, prop, value);
    }
  });
}

Pattern 4 — Read-only wrapper:

function readOnly(target) {
  return new Proxy(target, {
    set() { throw new Error('read-only'); },
    deleteProperty() { throw new Error('read-only'); },
    defineProperty() { throw new Error('read-only'); }
  });
}

Pattern 5 — Hidden private fields:

function hide(target, hidden) {
  return new Proxy(target, {
    has(obj, prop) {
      return !hidden.has(prop) && Reflect.has(obj, prop);
    },
    ownKeys(obj) {
      return Reflect.ownKeys(obj).filter(k => !hidden.has(k));
    },
    getOwnPropertyDescriptor(obj, prop) {
      if (hidden.has(prop)) return undefined;
      return Reflect.getOwnPropertyDescriptor(obj, prop);
    }
  });
}

Pattern 6 — Lazy loading:

function lazy(getLoader) {
  let loaded = null;
  return new Proxy({}, {
    get(obj, prop) {
      loaded ??= getLoader();
      return Reflect.get(loaded, prop);
    }
  });
}

Pattern 7 — Function mocking:

function trackCalls(fn) {
  const calls = [];
  const proxy = new Proxy(fn, {
    apply(target, thisArg, args) {
      calls.push(args);
      return Reflect.apply(target, thisArg, args);
    }
  });
  proxy.calls = calls;
  return proxy;
}

const add = trackCalls((a, b) => a + b);
add(1, 2);
add(3, 4);
console.log(add.calls);
// [ [ [ 1, 2 ], [ 3, 4 ] ] ]

Pattern 8 — Array with negative indices:

function negIndex(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
        return target[target.length + Number(prop)];
      }
      return Reflect.get(target, prop);
    }
  });
}

Pattern 9 — Access control:

function withPermissions(target, perms) {
  return new Proxy(target, {
    get(obj, prop) {
      if (!perms.read?.includes(prop)) {
        throw new Error(`No read access to ${String(prop)}`);
      }
      return Reflect.get(obj, prop);
    },
    set(obj, prop, value) {
      if (!perms.write?.includes(prop)) {
        throw new Error(`No write access to ${String(prop)}`);
      }
      return Reflect.set(obj, prop, value);
    }
  });
}

Pattern 10 — Memoization on access:

function cached(compute) {
  const cache = new Map();
  return new Proxy({}, {
    get(obj, prop) {
      if (!cache.has(prop)) {
        cache.set(prop, compute(prop));
      }
      return cache.get(prop);
    }
  });
}

Pattern 11 — Type coercion with get:

const stringify = new Proxy({ x: 10, y: 20 }, {
  get(target, prop) {
    if (prop === Symbol.toPrimitive) {
      return () => JSON.stringify(target);
    }
    return Reflect.get(target, prop);
  }
});

console.log(`${stringify}`);
// [ '{"x":10,"y":20}' ]

Pattern 12 — Combining Reflect with inheritance:

const parent = { greet() { return 'hi'; } };

const child = new Proxy(Object.create(parent), {
  get(target, prop, receiver) {
    console.log(`Accessing ${String(prop)}`);
    return Reflect.get(target, prop, receiver);
  }
});

console.log(child.greet());
// [ Accessing greet ]
// [ 'hi' ]

Reflect.get with receiver ensures inherited getters see the proxy.

Pattern 13 — Defaults with has:

function defaultValue(target, fallback) {
  return new Proxy(target, {
    get(obj, prop) {
      if (prop in obj) return obj[prop];
      return fallback;
    }
  });
}

Pattern 14 — Revocable proxy:

const { proxy, revoke } = Proxy.revocable({ name: 'Alice' }, {});

console.log(proxy.name);
// [ 'Alice' ]

revoke();
console.log(proxy.name);
// TypeError: Cannot perform 'get' on a proxy that has been revoked

Pattern 15 — Immutable state:

function immutable(target) {
  return new Proxy(target, {
    set() { throw new Error('immutable'); },
    deleteProperty() { throw new Error('immutable'); },
    defineProperty() { throw new Error('immutable'); },
    setPrototypeOf() { throw new Error('immutable'); }
  });
}

Pattern 16 — Auto-populating objects:

const autoFill = new Proxy({}, {
  get(target, prop) {
    if (!(prop in target)) {
      target[prop] = { name: String(prop) };
    }
    return target[prop];
  }
});

console.log(autoFill.users);
// [ { name: 'users' } ]

console.log(autoFill.posts);
// [ { name: 'posts' } ]

Pattern 17 — Path tracking for state libraries:

function trackPath(obj, path = '') {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      const newPath = path ? `${path}.${String(prop)}` : String(prop);
      if (value && typeof value === 'object') {
        return trackPath(value, newPath);
      }
      return value;
    }
  });
}

Pattern 18 — Lazy nested objects:

const lazyObj = new Proxy({}, {
  get(target, prop) {
    target[prop] ??= new Proxy({}, {
      get(t, p) {
        return `${String(prop)}.${String(p)}`;
      }
    });
    return target[prop];
  }
});

console.log(lazyObj.user.name);
// [ 'user.name' ]

Pattern 19 — Function argument validation:

function validateArgs(fn, validators) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      validators.forEach((check, i) => {
        if (!check(args[i])) {
          throw new TypeError(`Argument ${i} invalid`);
        }
      });
      return Reflect.apply(target, thisArg, args);
    }
  });
}

Pattern 20 — Full observable example:

function observable(target, onChange) {
  const handler = {
    get(obj, prop, receiver) {
      const value = Reflect.get(obj, prop, receiver);
      if (value && typeof value === 'object') {
        return observable(value, onChange);
      }
      return value;
    },
    set(obj, prop, value, receiver) {
      const oldValue = obj[prop];
      const result = Reflect.set(obj, prop, value, receiver);
      if (oldValue !== value) {
        onChange(prop, value, oldValue);
      }
      return result;
    }
  };
  return new Proxy(target, handler);
}

const state = observable(
  { user: { name: 'Alice' } },
  (prop, val, old) => console.log(`${String(prop)}: ${old} → ${val}`)
);

state.user.name = 'Bob';
// [ name: Alice → Bob ]

When to use Proxy and Reflect:

SituationUse
Validation on writeProxy set
Defaults for readsProxy get
Logging accessProxy get / set
Read-only objectsProxy set throws
Hiding keysProxy ownKeys
Custom in behaviorProxy has
Function mockingProxy apply
Framework reactivityProxy get / set
Forwarding to defaultsReflect
Accessing descriptorsReflect
Calling functions reflectivelyReflect

Common pitfalls:

  • Proxy traps must match the target’s invariants — e.g., can’t report non-configurable props as missing
  • ownKeys must include non-configurable keys — otherwise TypeError
  • Reflect helps keep invariants — use it inside traps
  • Proxies are not transparent=== compares the proxy, not the target
  • Proxy performance is lower than plain objects — use sparingly
  • Revocable proxies can be disabled — Proxy.revocable

Complete Example Session

// ============================================
// PART 1: BASIC GET TRAP
// ============================================

const target = { name: 'Alice' };

const proxy = new Proxy(target, {
  get(obj, prop) {
    console.log(`Getting ${prop}`);
    return obj[prop];
  }
});

console.log(proxy.name);
// [ Getting name ]
// [ 'Alice' ]

// ============================================
// PART 2: SET TRAP
// ============================================

const proxy2 = new Proxy({}, {
  set(obj, prop, value) {
    console.log(`Setting ${prop} = ${value}`);
    obj[prop] = value;
    return true;
  }
});

proxy2.age = 30;
// [ Setting age = 30 ]

// ============================================
// PART 3: HAS TRAP
// ============================================

const proxy3 = new Proxy({ name: 'Alice' }, {
  has(obj, prop) {
    return prop in obj;
  }
});

console.log('name' in proxy3);
// [ true ]

console.log('age' in proxy3);
// [ false ]

// ============================================
// PART 4: DELETE TRAP
// ============================================

const proxy4 = new Proxy({ name: 'Alice' }, {
  deleteProperty(obj, prop) {
    console.log(`Deleting ${prop}`);
    return Reflect.deleteProperty(obj, prop);
  }
});

delete proxy4.name;
// [ Deleting name ]

// ============================================
// PART 5: VALIDATION
// ============================================

const strict = new Proxy({}, {
  set(obj, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('age must be a number');
    }
    return Reflect.set(obj, prop, value);
  }
});

strict.age = 30;
console.log(strict.age);
// [ 30 ]

try {
  strict.age = 'thirty';
} catch (err) {
  console.log(err.message);
}
// [ age must be a number ]

// ============================================
// PART 6: DEFAULTS
// ============================================

const defaults = new Proxy({}, {
  get(obj, prop) {
    return obj[prop] ?? `default-${String(prop)}`;
  }
});

console.log(defaults.name);
// [ 'default-name' ]

console.log(defaults.foo);
// [ 'default-foo' ]

// ============================================
// PART 7: REFLECT
// ============================================

const obj = { name: 'Alice' };

console.log(Reflect.get(obj, 'name'));
// [ 'Alice' ]

console.log(Reflect.has(obj, 'name'));
// [ true ]

Reflect.set(obj, 'age', 30);
console.log(obj.age);
// [ 30 ]

console.log(Reflect.ownKeys(obj));
// [ [ 'name', 'age' ] ]

// ============================================
// PART 8: APPLY TRAP
// ============================================

const fn = (a, b) => a + b;

const fnProxy = new Proxy(fn, {
  apply(target, thisArg, args) {
    console.log(`Called with ${args}`);
    return Reflect.apply(target, thisArg, args);
  }
});

console.log(fnProxy(2, 3));
// [ Called with 2,3 ]
// [ 5 ]

// ============================================
// PART 9: CONSTRUCT TRAP
// ============================================

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

const CtorProxy = new Proxy(Person, {
  construct(target, args) {
    console.log(`Constructing ${args}`);
    return Reflect.construct(target, args);
  }
});

const p = new CtorProxy('Alice');
// [ Constructing Alice ]

console.log(p.name);
// [ 'Alice' ]

// ============================================
// PART 10: READ-ONLY
// ============================================

function readOnly(target) {
  return new Proxy(target, {
    set() { throw new Error('read-only'); },
    deleteProperty() { throw new Error('read-only'); }
  });
}

const ro = readOnly({ name: 'Alice' });

console.log(ro.name);
// [ 'Alice' ]

try {
  ro.name = 'Bob';
} catch (err) {
  console.log(err.message);
}
// [ read-only ]

// ============================================
// PART 11: LOGGING PROXY
// ============================================

function log(target) {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`get ${String(prop)}`);
      return Reflect.get(obj, prop);
    },
    set(obj, prop, value) {
      console.log(`set ${String(prop)} = ${value}`);
      return Reflect.set(obj, prop, value);
    }
  });
}

const logged = log({ name: 'Alice' });
logged.name;
// [ get name ]

logged.age = 30;
// [ set age = 30 ]

// ============================================
// PART 12: OBSERVABLE
// ============================================

function observable(target, onChange) {
  return new Proxy(target, {
    get(obj, prop, receiver) {
      return Reflect.get(obj, prop, receiver);
    },
    set(obj, prop, value, receiver) {
      const old = obj[prop];
      const result = Reflect.set(obj, prop, value, receiver);
      if (old !== value) onChange(prop, value, old);
      return result;
    }
  });
}

const state = observable({ count: 0 }, (prop, val, old) => {
  console.log(`${String(prop)}: ${old} → ${val}`);
});

state.count = 1;
// [ count: 0 → 1 ]

// ============================================
// PART 13: REVOCABLE
// ============================================

const { proxy: revocableProxy, revoke } = Proxy.revocable({ a: 1 }, {});

console.log(revocableProxy.a);
// [ 1 ]

revoke();

try {
  console.log(revocableProxy.a);
} catch (err) {
  console.log(err.message);
}
// [ Cannot perform 'get' on a proxy that has been revoked ]

// ============================================
// PART 14: NEGATIVE INDEX ARRAY
// ============================================

function negIndex(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
        return target[target.length + Number(prop)];
      }
      return Reflect.get(target, prop);
    }
  });
}

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

// ============================================
// PART 15: HIDE KEYS
// ============================================

const hidden = new Set(['secret']);
const hideProxy = new Proxy({ a: 1, secret: 2 }, {
  has(obj, prop) {
    return !hidden.has(prop) && Reflect.has(obj, prop);
  },
  ownKeys(obj) {
    return Reflect.ownKeys(obj).filter(k => !hidden.has(k));
  },
  getOwnPropertyDescriptor(obj, prop) {
    if (hidden.has(prop)) return undefined;
    return Reflect.getOwnPropertyDescriptor(obj, prop);
  }
});

console.log(Object.keys(hideProxy));
// [ [ 'a' ] ]

console.log('secret' in hideProxy);
// [ false ]

// ============================================
// PART 16: FUNCTION TRACKING
// ============================================

function trackCalls(fn) {
  const calls = [];
  const proxy = new Proxy(fn, {
    apply(target, thisArg, args) {
      calls.push(args);
      return Reflect.apply(target, thisArg, args);
    }
  });
  proxy.calls = calls;
  return proxy;
}

const add = trackCalls((a, b) => a + b);
add(1, 2);
add(3, 4);
console.log(add.calls.length);
// [ 2 ]

// ============================================
// PART 17: AUTO-VIVIFICATION
// ============================================

const auto = new Proxy({}, {
  get(target, prop) {
    if (!(prop in target)) {
      target[prop] = {};
    }
    return target[prop];
  }
});

auto.users.name = 'Alice';
console.log(auto.users.name);
// [ 'Alice' ]

// ============================================
// PART 18: REFLECT.APPLY
// ============================================

function greet(greeting) {
  return `${greeting}, ${this.name}`;
}

const result = Reflect.apply(greet, { name: 'Alice' }, ['Hello']);
console.log(result);
// [ 'Hello, Alice' ]

// ============================================
// PART 19: REFLECT.CONSTRUCT
// ============================================

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

const pt = Reflect.construct(Point, [1, 2]);
console.log(pt.x, pt.y);
// [ 1 2 ]

// ============================================
// PART 20: FULL SCRIPT
// ============================================

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

const proxy58 = new Proxy(target58, {
  get(obj, prop) {
    console.log(`Getting ${String(prop)}`);
    return obj[prop];
  },
  set(obj, prop, value) {
    console.log(`Setting ${String(prop)} to ${value}`);
    obj[prop] = value;
    return true;
  }
});

console.log(proxy58.name);
proxy58.age = 31;
console.log(target58.age);

const proxy58b = new Proxy(target58, {
  has(obj, prop) { return prop in obj; },
  deleteProperty(obj, prop) { return Reflect.deleteProperty(obj, prop); }
});

console.log('name' in proxy58b);
delete proxy58b.age;
console.log('age' in proxy58b);

const validator58 = new Proxy({}, {
  set(obj, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    obj[prop] = value;
    return true;
  }
});

validator58.age = 30;
console.log(validator58.age);

const defaults58 = new Proxy({}, {
  get(obj, prop) {
    return obj[prop] ?? `default-${String(prop)}`;
  }
});

console.log(defaults58.name);
console.log(defaults58.foo);

const reflectTest58 = { name: 'Alice' };

console.log(Reflect.get(reflectTest58, 'name'));
console.log(Reflect.has(reflectTest58, 'name'));
Reflect.set(reflectTest58, 'age', 30);
console.log(reflectTest58.age);
console.log(Reflect.ownKeys(reflectTest58));

Quick Reference

Proxy Basics

SyntaxMeaning
new Proxy(target, handler)Create proxy
Proxy.revocable(target, handler)Revocable proxy
proxy.operationIntercepted
target.operationOriginal, untouched

All Traps

TrapIntercepts
getRead
setWrite
hasin
deletePropertydelete
ownKeysKey enumeration
getOwnPropertyDescriptorDescriptor read
definePropertyDescriptor write
getPrototypeOfPrototype read
setPrototypeOfPrototype write
isExtensibleExtensibility check
preventExtensionsLock object
applyFunction call
constructnew

Reflect Methods

MethodPurpose
Reflect.get(o, p)Read
Reflect.set(o, p, v)Write
Reflect.has(o, p)in
Reflect.deleteProperty(o, p)Delete
Reflect.ownKeys(o)All keys
Reflect.getOwnPropertyDescriptor(o, p)Descriptor
Reflect.defineProperty(o, p, d)Define
Reflect.getPrototypeOf(o)Get proto
Reflect.setPrototypeOf(o, p)Set proto
Reflect.isExtensible(o)Check
Reflect.preventExtensions(o)Lock
Reflect.apply(fn, t, a)Call
Reflect.construct(C, a)New

Proxy vs Alternatives

FeaturedefinePropertyProxy
Per-property
Whole object
Function calls
in operator
delete
ownKeys
PerformanceFasterSlower

Common Patterns

PatternTrap
Validationset
Defaultsget
Loggingget, set
Read-onlyset throws
HidingownKeys, has
Mockingapply
Negative indicesget
Observabilityget, set
Lazy loadingget

Traps and their Reflect equivalents

TrapReflect
getReflect.get
setReflect.set
hasReflect.has
deletePropertyReflect.deleteProperty
ownKeysReflect.ownKeys
applyReflect.apply
constructReflect.construct

Best Practices

Do This:

// Use Reflect inside traps
get(target, prop, receiver) {
  return Reflect.get(target, prop, receiver);   // ✅
}

// Validate writes
set(target, prop, value) {
  if (!isValid(value)) throw new TypeError();
  return Reflect.set(target, prop, value);      // ✅
}

// Provide default forwarding
const proxy = new Proxy(target, {});            // ✅

// Use revocable when you need cleanup
const { proxy, revoke } = Proxy.revocable(target, handler);  // ✅

// Return true from set to allow writes
set() { return true; }                          // ✅

// Use receiver for inheritance-safe access
Reflect.get(target, prop, receiver);            // ✅

// Keep traps focused
get(target, prop) { log(prop); return target[prop]; }  // ✅

Don’t Do This:

// Don't forget to return from set
set(target, prop, value) {
  target[prop] = value;                         // ❌ returns undefined
  return true;                                  // ✅
}

// Don't violate invariants
get(target, prop) {
  return 'always';                              // ⚠️  may break
}

// Don't skip non-configurable keys in ownKeys
ownKeys() {
  return [];                                    // ❌ if non-configurable exist
}

// Don't use Proxy in hot loops
for (let i = 0; i < 1e6; i++) proxy.x;         // ❌ slow
// Use a plain object instead

// Don't wrap primitives
new Proxy(42, {});                              // ❌ TypeError

// Don't forget to handle receiver
get(target, prop) {
  return target[prop];                          // ⚠️  breaks inherited getters
}
Reflect.get(target, prop, receiver);            // ✅

// Don't proxy for simple cases
// If Object.defineProperty or a getter works, use that

Common Pitfalls

PitfallProblemSolution
Forgetting return true in setSilent failureReturn true
Not using ReflectBreaks invariantsUse Reflect.get/set
Proxy in hot loopsSlowAvoid or use plain
Revoked proxy accessTypeErrorGuard with try
Missing non-configurable keysTypeErrorInclude them
Traps don’t forwardBehavior breaksUse Reflect
=== compares proxyNot targetCompare .target if needed
Losing this in methodsWrong receiverUse Reflect.get(..., receiver)

Real-World Examples

1. Basic Get

const p = new Proxy({ name: 'Alice' }, {
  get(t, prop) { return t[prop]; }
});

console.log(p.name);
// [ 'Alice' ]

2. Basic Set

const p = new Proxy({}, {
  set(t, prop, v) { t[prop] = v; return true; }
});

p.x = 1;
console.log(p.x);
// [ 1 ]

3. Has

const p = new Proxy({ a: 1 }, {
  has(t, prop) { return prop in t; }
});

console.log('a' in p);
// [ true ]

4. Delete

const p = new Proxy({ a: 1 }, {
  deleteProperty(t, prop) { return Reflect.deleteProperty(t, prop); }
});

delete p.a;
console.log('a' in p);
// [ false ]

5. Validation

const strict = new Proxy({}, {
  set(t, prop, v) {
    if (prop === 'age' && typeof v !== 'number') throw new TypeError();
    return Reflect.set(t, prop, v);
  }
});

6. Default Values

const d = new Proxy({}, {
  get(t, prop) { return t[prop] ?? 'default'; }
});

console.log(d.x);
// [ 'default' ]

7. Read-Only

const ro = new Proxy({ a: 1 }, {
  set() { throw new Error('read-only'); }
});

8. Logging

const log = new Proxy({ name: 'Alice' }, {
  get(t, prop) { console.log(`get ${String(prop)}`); return t[prop]; }
});

log.name;
// [ get name ]

9. Apply

const fn = new Proxy((a, b) => a + b, {
  apply(t, thisArg, args) { return Reflect.apply(t, thisArg, args); }
});

10. Construct

class Person {}
const Ctor = new Proxy(Person, {
  construct(t, args) { return Reflect.construct(t, args); }
});

11. Observable

const state = new Proxy({ count: 0 }, {
  set(t, prop, v) {
    console.log(`${String(prop)}: ${t[prop]} → ${v}`);
    return Reflect.set(t, prop, v);
  }
});

state.count = 1;
// [ count: 0 → 1 ]

12. Revocable

const { proxy, revoke } = Proxy.revocable({ a: 1 }, {});
revoke();
// proxy.a throws

13. Negative Index

const arr = new Proxy([1, 2, 3], {
  get(t, prop) {
    if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
      return t[t.length + Number(prop)];
    }
    return Reflect.get(t, prop);
  }
});

console.log(arr[-1]);
// [ 3 ]

14. Hide Keys

const hidden = new Set(['secret']);
const p = new Proxy({ a: 1, secret: 2 }, {
  ownKeys(t) { return Reflect.ownKeys(t).filter(k => !hidden.has(k)); },
  getOwnPropertyDescriptor(t, prop) {
    if (hidden.has(prop)) return undefined;
    return Reflect.getOwnPropertyDescriptor(t, prop);
  }
});

console.log(Object.keys(p));
// [ [ 'a' ] ]

15. Function Mocking

const calls = [];
const mock = new Proxy(fn, {
  apply(t, thisArg, args) {
    calls.push(args);
    return Reflect.apply(t, thisArg, args);
  }
});

16. Reflect.get

const o = { a: 1 };
console.log(Reflect.get(o, 'a'));
// [ 1 ]

17. Reflect.has

console.log(Reflect.has({ a: 1 }, 'a'));
// [ true ]

18. Reflect.ownKeys

console.log(Reflect.ownKeys({ a: 1, b: 2 }));
// [ [ 'a', 'b' ] ]

19. Reflect.apply

function f(x) { return this.v + x; }
console.log(Reflect.apply(f, { v: 10 }, [5]));
// [ 15 ]

20. Full Script

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

const proxy58 = new Proxy(target58, {
  get(obj, prop) {
    console.log(`Getting ${String(prop)}`);
    return obj[prop];
  },
  set(obj, prop, value) {
    console.log(`Setting ${String(prop)} to ${value}`);
    obj[prop] = value;
    return true;
  }
});

console.log(proxy58.name);
proxy58.age = 31;
console.log(target58.age);

const proxy58b = new Proxy(target58, {
  has(obj, prop) { return prop in obj; },
  deleteProperty(obj, prop) { return Reflect.deleteProperty(obj, prop); }
});

console.log('name' in proxy58b);
delete proxy58b.age;
console.log('age' in proxy58b);

const validator58 = new Proxy({}, {
  set(obj, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    obj[prop] = value;
    return true;
  }
});

validator58.age = 30;
console.log(validator58.age);

const defaults58 = new Proxy({}, {
  get(obj, prop) {
    return obj[prop] ?? `default-${String(prop)}`;
  }
});

console.log(defaults58.name);
console.log(defaults58.foo);

const reflectTest58 = { name: 'Alice' };

console.log(Reflect.get(reflectTest58, 'name'));
console.log(Reflect.has(reflectTest58, 'name'));
Reflect.set(reflectTest58, 'age', 30);
console.log(reflectTest58.age);
console.log(Reflect.ownKeys(reflectTest58));

Visual: How a Proxy Works

┌──────────────────────────────────────────────┐
│  You: proxy.name                             │
│         │                                    │
│         ▼                                    │
│  ┌─────────────────────────────────────┐     │
│  │       Proxy Handler                 │     │
│  │                                     │     │
│  │  get(target, 'name')                │     │
│  │      │                              │     │
│  │      ▼                              │     │
│  │  your code runs                     │     │
│  │      │                              │     │
│  │      └──► return value              │     │
│  └──────────────┬──────────────────────┘     │
│                 │                            │
│                 ▼                            │
│              Result                          │
│                                              │
│  Target is only touched if the handler       │
│  forwards to it (usually via Reflect)        │
│                                              │
└──────────────────────────────────────────────┘

Visual: Proxy vs Target

┌──────────────────────────────────────────────┐
│  target = { name: 'Alice' }                  │
│                                              │
│  proxy = new Proxy(target, handler)          │
│                                              │
│  proxy  ─────► handler ─────► target         │
│                                              │
│  Operations on proxy go through handler      │
│  Operations on target stay direct            │
│                                              │
│  proxy !== target                            │
│                                              │
└──────────────────────────────────────────────┘

Visual: Reflect in Traps

┌──────────────────────────────────────────────┐
│  const handler = {                           │
│    get(target, prop, receiver) {             │
│      // custom logic                         │
│      return Reflect.get(target, prop, recv); │
│    }                                         │
│  };                                          │
│                                              │
│  Reflect.get = the default behavior          │
│  Use it to forward when you don't            │
│  want to override                             │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptSyntaxPurpose
Proxynew Proxy(target, handler)Intercept operations
RevocableProxy.revocable(t, h)Disable later
getget(target, prop, receiver)Read
setset(target, prop, value, receiver)Write
hashas(target, prop)in
deletePropertydeleteProperty(t, p)delete
ownKeysownKeys(target)Key enumeration
applyapply(target, thisArg, args)Function call
constructconstruct(target, args)new
Reflect.getReflect.get(o, p)Read default
Reflect.setReflect.set(o, p, v)Write default
Reflect.hasReflect.has(o, p)in default
Reflect.ownKeysReflect.ownKeys(o)Keys default
Reflect.applyReflect.apply(fn, t, a)Call default
Reflect.constructReflect.construct(C, a)New default

Key takeaways:

  • Proxy wraps an object and intercepts its operations via traps
  • The target object is unchanged — the proxy is a separate wrapper
  • get and set are the most-used traps — for reading and writing
  • has, deleteProperty, ownKeys intercept in, delete, and enumeration
  • apply and construct work on function proxies
  • Reflect provides the default behavior for every trap — use it inside handlers
  • Reflect.get(target, prop, receiver) is the correct way to forward
  • Use Proxy.revocable when you need to disable the proxy later
  • Common uses: validation, defaults, observability, read-only, logging, frameworks
  • Watch performance — proxies are slower than plain objects
  • Respect invariants — non-configurable properties must be reported correctly
  • Reflect is not just for proxies — it’s a clean function API for object operations

Remember: Proxy and Reflect are the metaprogramming foundation of modern JavaScript. Proxy intercepts; Reflect forwards. Together they let you build objects that behave in custom ways — validating writes, providing defaults, tracking reads, mocking functions, hiding keys. Vue 3, MobX, and many libraries depend on them. Use them thoughtfully — they’re powerful but slower, and invariants must be respected. Master Proxy and Reflect, and you can shape how objects behave in ways ordinary JavaScript can’t.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!