JS: Proxy constructor

By Xah Lee. Date: . Last updated: .

(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 xobj = { dog: 3 };

// create a handler.
// handler is a object. It has methods to change the target object behavior.
// here, we change the property access behavior
const handler = {
 get: (xtarget, xprop) => {
  console.log("trap called with args: ", xtarget, xprop);
  return xtarget[xprop];
 },
};

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

console.log(xx.dog);
// trap called with args:  { dog: 3 } dog
// 3

Basic syntax

new Proxy(target, handler)

List of traps

These are special property names in the handler object. Their value must be a function, with proper parameters and return value.

for function objects

Property lookup

get(target, property, receiver) {body}

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

if the property is a Getter Property, the receiver is passed to the getter function as this-binding. Normally the value is same as target.

🟒 tip: in the trap body, great to use Reflect.get

// example. proxy with get trap

const xobj = { pp: 30 };

const xx = new Proxy(xobj, {
 get(xobj, xkey, thisValue) {
  console.log("trap called with args: ", xobj, xkey, thisValue);
  return Reflect.get(xobj, xkey, thisValue);
 },
});

console.log(xx.pp);
// trap called with args:  { pp: 30 } pp { pp: 30 }
// 30

Example. array with negative index

/*
create a new type of array that can be indexed by negative index.
this simulates Array.prototype.at
*/

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

const xx = make_neg_index_array(["dog", "cat", "bird"]);
console.assert(xx[-1] === "bird");

Property assignment

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

traps property assignment.

if the property is a Setter Property, the receiver is passed to the setter function as this-binding. Normally the value is same as target.

🟒 tip: in the trap body, great to use Reflect.set

// example. proxy with set trap
const xobj = { pp: 2 };

const xx = new Proxy(xobj, {
 set(xtarget, xprop, xvalue, xreceiver) {
  console.log("trap called with args: ", xtarget, xprop, xvalue, xreceiver);

  // must return true if setting property is successful, else return false.
  if (xvalue > 0) {
   return Reflect.set(xtarget, xprop, xvalue, xreceiver);
  } else {
   return false;
  }
 },
});

// sucess example because value is positive
xx.aa = 3;
// trap called with args:  { pp: 2 } aa 3 { pp: 2 }

// fail example because value is negative
try {
 xx.bb = -3;
} catch (xerror) {
 console.log(xerror);
}
// trap called with args:  { pp: 2, aa: 3 } bb -3 { pp: 2, aa: 3 }
// TypeError: 'set' on proxy: trap returned falsish for property 'bb'

Checking property existance in prototype chain

has(target, property)

Intercepts any operation that tests property existance in prototype chain.

// example. proxy with β€œhas” trap (check property existance)

const xobj = { pp: 9 };

const handler = {
 has: (xtarget, xprop) => {
  console.log("trap called with args: ", xtarget, xprop);
  return Reflect.has(xtarget, xprop);
 },
};

const xx = new Proxy(xobj, handler);

console.log("pp" in xx);
// trap called with args:  { pp: 9 } pp
// true

Delete property

deleteProperty(target, property)

Intercepts all operations that deletes property.

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

// prevent deletion of dog

const xx = new Proxy(xobj, {
 deleteProperty(xtarget, xprop) {
  console.log("trap called with args:", xtarget, xprop);
  if (xprop === "dog") {
   // if delete failed, must return false. which throws an error
   return false;
  } else {
   return Reflect.deleteProperty(xtarget, xprop);
  }
 },
});

delete xx.cat;
// trap called with args: { dog: 5, cat: 3, bird: 4 } cat

// fail example, because we prevent deleting property dog
try {
 delete xx.dog;
} catch (xerror) {
 console.log(xerror);
}
// trap called with args: { dog: 5, bird: 4 } dog
// TypeError: 'deleteProperty' on proxy: trap returned falsish for property 'dog'

Get all own keys

This trap intercepts these operations:

ownKeys(target)

it must return an array-like object where each element is either a String or a Symbol containing no duplicate items.

const xobj = { dog: 5, cat: 3 };

const xx = new Proxy(xobj, {
 ownKeys(xtarget) {
  console.log("trap called with args:", xtarget);
  return Reflect.ownKeys(xtarget);
 },
});

console.log(Object.keys(xobj));
// [ "dog", "cat" ]

console.log(Object.keys(xx));
// trap called with args: { dog: 5, cat: 3 }
// [ "dog", "cat" ]

Define property with descriptor

defineProperty(target, key, desc)

traps:

// the target object
const xobj = {};

const handler = {
 defineProperty(xtarget, xkey, xdesc) {
  console.log("trap called with args: ", xtarget, xkey, xdesc);
  return Reflect.defineProperty(xtarget, xkey, xdesc);
 },
};

const xx = new Proxy(xobj, handler);

const desc = {
 value: 5,
 writable: true,
 enumerable: true,
 configurable: true,
};

Reflect.defineProperty(xx, "cat", desc);
// trap called with args:  {} cat { value: 5, writable: true, enumerable: true, configurable: true }

console.log(xx);
// { cat: 5 }

// s------------------------------

// check defineProperty trap works with Object.defineProperties

Object.defineProperties(
 xx,
 {
  bird: {
   value: 1,
   writable: true,
   enumerable: true,
   configurable: true,
  },
  fish: {
   value: 2,
   writable: true,
   enumerable: true,
   configurable: true,
  },
 },
);

console.log(xx);
// trap called with args:  { cat: 5 } bird { value: 1, writable: true, enumerable: true, configurable: true }
// trap called with args:  { cat: 5, bird: 1 } fish { value: 2, writable: true, enumerable: true, configurable: true }

// { cat: 5, bird: 1, fish: 2 }

Get own property descriptor

getOwnPropertyDescriptor(target, key)

traps:

// the target object
const xobj = { dog: 3 };

const handler = {
 getOwnPropertyDescriptor(xtarget, xkey) {
  console.log("trap called with args: ", xtarget, xkey);
  return Reflect.getOwnPropertyDescriptor(xtarget, xkey);
 },
};

const xx = new Proxy(xobj, handler);

console.log(Reflect.getOwnPropertyDescriptor(xx, "dog"));
// trap called with args:  { dog: 3 } dog
// { value: 3, writable: true, enumerable: true, configurable: true }

Is extensible

isExtensible(target)

traps:

// the target object
const xobj = { dog: 3 };

const handler = {
 isExtensible(xtarget) {
  console.log("trap called with args: ", xtarget);
  return Reflect.isExtensible(xtarget);
 },
};

const xx = new Proxy(xobj, handler);

console.log(
 Reflect.isExtensible(xx, "dog"),
);
// trap called with args:  { dog: 3 }
// true

Prevent extensions

preventExtensions(target)

traps:

// the target object
const xobj = { dog: 3 };

const handler = {
 preventExtensions(xtarget) {
  console.log("trap called with args: ", xtarget);
  return Reflect.preventExtensions(xtarget);
 },
};

const xx = new Proxy(xobj, handler);

console.log(
 Reflect.preventExtensions(xx),
);
// trap called with args:  { dog: 3 }
// true

Get prototype of

getPrototypeOf(target)

traps:

// the target object
const xobj = { dog: 3 };

const handler = {
 getPrototypeOf(xtarget) {
  console.log("trap called with args: ", xtarget);
  return Reflect.getPrototypeOf(xtarget);
 },
};

const xx = new Proxy(xobj, handler);

console.log(
 Reflect.getPrototypeOf(xx),
);
// trap called with args:  { dog: 3 }
// [Object: null prototype] {}

Set prototype of

setPrototypeOf(target, parentObj)

traps:

// the target object
const xobj = { length: 3 };

const handler = {
 setPrototypeOf(xtarget, xdad) {
  console.log("trap called with args: ", xtarget, xdad);
  return Reflect.setPrototypeOf(xtarget, xdad);
 },
};

const xx = new Proxy(xobj, handler);

Reflect.setPrototypeOf(xx, Array.prototype);
// trap called with args:  { length: 3 } Object(0) []

console.log(xx.fill(0));
// Array { "0": 0, "1": 0, "2": 0, length: 3 }

Apply function for function objects

apply(target, thisArg, argumentsList)

Intercepts function application.

🟒 tip: in the body, great to use JS: Reflect.apply

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

const handler = {
 apply(target, thisBinding, argArray) {
  console.log("trap called with args:", target, thisBinding, argArray);
  return Reflect.apply(target, thisBinding, argArray);
 },
};

const ff = new Proxy(fun_add, handler);

console.log(ff(3, 4));
// trap called with args: [Function: fun_add] undefined [ 3, 4 ]
// 7

Example. Cache Function (Memoization)

function make_memorize_func(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);
  },
 });
}

// expensive function
const xfunc = (x) => {
 console.log("computing...");
 return x + 1;
};

const memorized_f = make_memorize_func(xfunc);

console.log(memorized_f(5));
// computing...
// 6

// from cache
console.log(memorized_f(5));
// 6

The β€œnew” operator

construct(target, argumentsList, newTarget)

traps:

class Burger {
 constructor(x) {
  this.meat = x;
 }
}

const Burger_trap = new Proxy(Burger, {
 construct(xtarget, argsArray, newTarget) {
  console.log("trap called with args:", xtarget, argsArray, newTarget);
  return Reflect.construct(xtarget, argsArray, newTarget);
 },
});

const my_burger = new Burger_trap("beef");
// trap called with args: [class Burger] [ "beef" ] [class Burger]

console.log(my_burger);
// Burger { meat: "beef" }