JS: Proxy constructor
(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)
target: Any object, array, function, or even another proxy.handler: An object with methods that intercept operations. The special property names are known as traps.
List of traps
These are special property names in the handler object. Their value must be a function, with proper parameters and return value.
- get
- set
- has
- deleteProperty
- ownKeys
- defineProperty
- getOwnPropertyDescriptor
- isExtensible
- preventExtensions
- getPrototypeOf
- setPrototypeOf
for function objects
- apply
- construct
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:
- JS: Object .getOwnPropertyNames
- JS: Object .getOwnPropertySymbols
- JS: Object.keys
- JS: Reflect.ownKeys
- JS: for-in Loop π
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" }