skip to content
nasty_bytes()

Proxy & Reflect

/ 3 min read

What’s the point?

Proxy is an ECMAScript6 mechanism for developers to intercept internal operations on an object such as property access, assignment, enumeration, function invocation, etc. This allows a developer to alter the default behavior of an operation or to perform a custom side-effect. Proxies are used for:

  • Logging and debugging
  • Validation
  • Data Binding
  • Memoization (caching)
  • Act as API wrapper
  • Metaprogramming
  • … and to achieve various other patterns.

Proxy and Reflect

Javascript provides a Proxy object to allow a developer to intercept any low-level internal methods ([[set]], [[get]], [[hasProperty]], etc.) via traps performed against a target object. These internal methods, denoted by double square brackets, are not directly accessible. A Proxy object wraps a target object thus acting as a gatekeeper to intercept operations defined within a handler object. Proxy’s kissing cousin, the reflection API, is a group of methods that reflect the default low-level behavior of an overridden trap. A Reflect API method is typically called within a trap to access the internal default operation after altering default behavior or performing a custom side-effect.

👨‍🍳

Just a note ...

All Proxy's traps have a corresponding `Reflect` **method** that accept the same arguments.

A list of internal methods and their corresponding traps can be found on MDN

By default, if a trap is not defined on the handler, then the operation will passthrough to the target. For example, a proxy initialized with an empty handler will behave exactly as the target. It is possible to understand this by defining get, but not set; therefore intercepting get, but allowing set to passthrough to the target.

1
const target = {
2
planet: "Earth",
3
};
4
5
const handler = {
6
// Intercept get
7
get(target, prop, receiver) {
8
// Custom side effect
9
console.log(`Hello, ${target[key]}`);
10
11
// Call default internal method operation with arguments
12
return Reflect.get(...arguments);
13
},
14
};
15
16
const proxiedObject = new Proxy(target, handler);
17
18
proxiedObject.planet; // > Hello, Earth
19
proxiedObject.planet = "Mars";

Prior to ECMAScript 6, Object.defineProperty() would allow you to intercept property access, albeit awkwardly. Proxy simplifies this without having to directly modify or redefine access when new properties are set.

1
function logPropertyAccess(target) {
2
const data = Object.create(null);
3
4
for (var prop in target) {
5
if (target.hasOwnProperty(prop)) data[prop] = target[prop];
6
Object.defineProperty(target, prop, {
7
get: function () {
8
console.log("get: ", prop);
9
return data[prop];
10
},
11
set: function (value) {
12
console.log("set: " + prop + ": " + value);
13
data[prop] = value;
14
},
15
});
16
}
17
18
return target;
19
}
20
21
const target = {
22
planet: "Earth",
23
};
24
25
const proxied = logPropertyAccess(target);
26
27
proxied.planet;
28
proxied.planet = "Mercury";

Examples

Validate a Property

Proxy can provide for a simple approach to object property validation. For example, if we have a need to validate a property for only a numeric value e.g. age, amount, count, we can define set within the handler to throw an exception if a value is not of type number.

1
const proxied = new Proxy(
2
{},
3
{
4
set(target, key, value, receiver) {
5
if (typeof value !== "number") throw new TypeError("Only numbers are allowed");
6
7
return Reflect.set(target, key, value, receiver);
8
},
9
},
10
);
11
12
proxied.foo = "Hello, World!"; // > TypeError exception

Data Binding

A primitive example of data-binding can be demonstrated by observing the change of a proxied array via a callback. This is conceptually similar to watch() in Vue.js.

1
const proxyArray = function (array, callback) {
2
return new Proxy(array, {
3
set(target, key, value, receiver) {
4
const oldValue = [...target];
5
const val = Reflect.set(target, key, value, receiver);
6
const newValue = [...target];
7
8
if (key !== "length") callback(newValue, oldValue);
9
10
return val;
11
},
12
});
13
};
14
15
const proxiedArray = proxyArray([], (newValue, oldValue) => {
16
console.log({ newValue, oldValue });
17
});
18
19
proxiedArray.push("Mercury");
20
proxiedArray.push("Venus");
21
proxiedArray.push("Earth");