JS: Proxy

By Xah Lee. Date: . Last updated: .
xtodo

(new in ECMAScript 2015)

What is a proxy

Proxy is a feature in JavaScript that is like a wrapper to objects. It lets you create a new object, based on a existing target object, and the new object behaves like the target object, except you can change any of its behavior, such as getting property value, setting property value, delete property, function call (when the target object is a function), constructor behavior, object extensibility, etc.

// the target object
const my_obj = { name: "Alice" };

// create a handler.
// handler is a special object syntax, to change the target object behavior.
// here, we change the property access behavior
const handler = {
 get(xtarget, xprop) {
  console.log(`Getting ${xprop}`);
  return xtarget[xprop];
 },
};

// create a proxy
const proxy = new Proxy(my_obj, handler);

console.log(proxy.name);
// Getting name
// Alice

Basic syntax

new Proxy(target, handler)

List of traps

for function objects

Example. get method

get(target, property, receiver) {body}

traps property access (e.g. obj.prop).

the receiver is the this-binding that a getter property function would receive. Normally the value is same as target.

// example. proxy with get method

const xobj = { pp: 30 };

const new_obj = new Proxy(xobj, {
 get(aa, bb, cc) {
  return [aa, bb, cc];
 },
});

console.log(new_obj.pp);
// [ { pp: 30 }, "pp", { pp: 30 } ]

trap property assignment

set(target, property, value, receiver) {body}

traps property assignment.

the receiver is the this-binding that a setter property function would receive. Normally the value is same as target.

// example. proxy with set method
const xobj = {};

const xnew = new Proxy(xobj, {
 set(xtarget, xprop, xvalue) {
  console.log(xtarget, xprop, xvalue);
  if (xvalue > 0) {
   // must return true on success
   return true;
  } else {
   return false;
  }
 },
});

xnew.aa = 3;
// prints
// {} aa 3

// xnew.bb = -3;
// error: Uncaught (in promise) TypeError: 'set' on proxy: trap returned falsish for property 'bb'

trap checking property existance

has(target, property)

Intercepts the in operator.

const xobj = { _password: "12345" };

const newobj = new Proxy(xobj, {
 has(xtarget, xprop) {
  if (xprop.startsWith("_")) return false; // hide private props
  return xprop in xtarget;
 },
});

console.log("_password" in newobj);
// false
console.log("toString" in newobj);
// true (inherited)
xtodo

(ai answer)

3. most useful traps

deleteProperty(target, property)

const xdata = { dog: 5, cat: 3, bird: 4 };

// prevent deletion of dog

const xnew_data = new Proxy(xdata, {
 deleteProperty(target, prop) {
  if (prop === "dog") {
   console.log("dog hit");
   // return false;
  }
  delete target[prop];
  // return true;
 },
});

delete xnew_data.cat;
// delete xnew_data.dog;

// error: Uncaught (in promise) TypeError: 'deleteProperty' on proxy: trap returned falsish for property 'cat'

apply(target, thisArg, argumentsList) – For functions

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

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

console.log(loggedAdd(3, 4)); // Called with 3,4 → 7

construct(target, argumentsList, newTarget) – For new

class User {
 constructor(name) {
  this.first_name = name;
 }
}

const LoggedUser = new Proxy(User, {
 construct(xtarget, args) {
  console.log(`Creating new ${xtarget.name} with`, args);
  return new xtarget(...args);
 },
});

const u = new LoggedUser("Alice");

4. other notable traps

5. practical examples

1. Negative Array Indexing (like Python)

function createNegativeArray(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      const index = Number(prop);
      if (index < 0) {
        return target[target.length + index];
      }
      return target[prop];
    }
  });
}

const fruits = createNegativeArray(["apple", "banana", "cherry"]);
console.log(fruits[-1]); // "cherry"
console.log(fruits[-2]); // "banana"

2. Observable / Reactive (simple version)

function makeObservable(target) {
  const observers = new Set();

  return new Proxy(target, {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      observers.forEach(observer => observer({ prop, value }));
      return result;
    },
    subscribe(callback) {
      observers.add(callback);
      return () => observers.delete(callback);
    }
  });
}

const state = makeObservable({ count: 0 });
state.subscribe(({ prop, value }) => console.log(`${prop} changed to ${value}`));

state.count = 42; // triggers callback

3. Default Values / Fallbacks

const config = new Proxy({}, {
  get(target, prop) {
    return prop in target ? target[prop] : `default_${prop}`;
  }
});

console.log(config.theme); // "default_theme"
config.theme = "dark";
console.log(config.theme); // "dark"

4. Memoization / Caching

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

const expensive = memoize((x) => {
  console.log("Computing...");
  return x * x;
});

console.log(expensive(5)); // Computing... → 25
console.log(expensive(5)); // 25 (cached)

6. best practices & gotchas