2024-05-06 20:40:05 +00:00
|
|
|
|
---
|
2024-10-30 17:34:11 +00:00
|
|
|
|
date: 2020-10-22
|
2024-05-06 20:40:05 +00:00
|
|
|
|
id: c5010eb1-4ce2-4415-8421-7710daecad0a
|
|
|
|
|
title: JavaScript Proxies
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# Introduction
|
|
|
|
|
|
|
|
|
|
ECMAScript 6 proxies bring intercession to
|
|
|
|
|
[JavaScript](20200613170905-javascript). They work as follows. There are
|
|
|
|
|
many operations that you can perform on an
|
|
|
|
|
[object](20200826201605-objects) obj. For example:
|
|
|
|
|
|
|
|
|
|
- Getting the property `prop` of an object `obj` (`obj.prop`)
|
|
|
|
|
- Checking whether an object `obj` has a property `prop`
|
|
|
|
|
(`'prop' in obj`)
|
|
|
|
|
|
|
|
|
|
Proxies are special objects that allow you customize some of these
|
|
|
|
|
operations. A proxy is created with two parameters:
|
|
|
|
|
|
|
|
|
|
- `handler`: For each operation, there is a corresponding handler
|
|
|
|
|
method that – if present – performs that operation. Such a method
|
|
|
|
|
*intercepts* the operation (on its way to the target) and is called
|
|
|
|
|
a *trap* (a term borrowed from the domain of operating systems).
|
|
|
|
|
- `target`: If the handler doesn’t intercept an operation then it is
|
|
|
|
|
performed on the target. That is, it acts as a fallback for the
|
|
|
|
|
handler. In a way, the proxy wraps the target.
|
|
|
|
|
|
|
|
|
|
# Examples
|
|
|
|
|
|
|
|
|
|
In the following example the handler intercepts the operations `get` and
|
|
|
|
|
`has`:
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
const target = {};
|
|
|
|
|
const handler = {
|
|
|
|
|
/** Intercepts: getting properties */
|
|
|
|
|
get(target, propKey, receiver) {
|
|
|
|
|
console.log(`GET ${propKey}`);
|
|
|
|
|
return 123;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Intercepts: checking whether properties exist */
|
|
|
|
|
has(target, propKey) {
|
|
|
|
|
console.log(`HAS ${propKey}`);
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const proxy = new Proxy(target, handler);
|
|
|
|
|
|
|
|
|
|
console.log(proxy.foo); // 123
|
|
|
|
|
console.log("hello" in proxy); // true
|
|
|
|
|
|
|
|
|
|
proxy.bar = "abc";
|
|
|
|
|
console.log(target.bar); // "abc"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
# Use cases for proxies
|
|
|
|
|
|
|
|
|
|
## Tracing property acesses (get, set)
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
function tracePropAccess(obj, propKeys) {
|
|
|
|
|
const propKeySet = new Set(propKeys);
|
|
|
|
|
return new Proxy(obj, {
|
|
|
|
|
get(target, propKey, receiver) {
|
|
|
|
|
if (propKeySet.has(propKey)) {
|
|
|
|
|
console.log("GET " + propKey);
|
|
|
|
|
}
|
|
|
|
|
return Reflect.get(target, propKey, receiver);
|
|
|
|
|
},
|
|
|
|
|
set(target, propKey, value, receiver) {
|
|
|
|
|
if (propKeySet.has(propKey)) {
|
|
|
|
|
console.log("SET " + propKey + "=" + value);
|
|
|
|
|
}
|
|
|
|
|
return Reflect.set(target, propKey, value, receiver);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Point {
|
|
|
|
|
constructor(x, y) {
|
|
|
|
|
this.x = x;
|
|
|
|
|
this.y = y;
|
|
|
|
|
}
|
|
|
|
|
toString() {
|
|
|
|
|
return `Point(${this.x}, ${this.y})`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Trace accesses to properties `x` and `y`
|
|
|
|
|
let p = new Point(5, 7);
|
|
|
|
|
p = tracePropAccess(p, ["x", "y"]);
|
|
|
|
|
|
|
|
|
|
// GET x
|
|
|
|
|
// 4
|
|
|
|
|
console.log(p.x);
|
|
|
|
|
|
|
|
|
|
// SET x=21
|
|
|
|
|
// 21
|
|
|
|
|
console.log((p.x = 21));
|
|
|
|
|
|
|
|
|
|
// GET x
|
|
|
|
|
// GET y
|
|
|
|
|
// Point(21, 7)
|
|
|
|
|
console.log(p.toString());
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Warning about unknown properties (get,set)
|
|
|
|
|
|
|
|
|
|
When it comes to accessing properties, JavaScript is very forgiving. For
|
|
|
|
|
example, if you try to read a property and misspell its name, you don’t
|
|
|
|
|
get an exception, you get the result `undefined`. You can use proxies to
|
|
|
|
|
get an exception in such a case. This works as follows. We make the
|
|
|
|
|
proxy a prototype of an object.
|
|
|
|
|
|
|
|
|
|
If a property isn’t found in the object, the `get` trap of the proxy is
|
|
|
|
|
triggered. If the property doesn’t even exist in the prototype chain
|
|
|
|
|
after the proxy, it really is missing and we throw an exception.
|
|
|
|
|
Otherwise, we return the value of the inherited property. We do so by
|
|
|
|
|
forwarding the `get` operation to the target (the prototype of the
|
|
|
|
|
target is also the prototype of the proxy).
|
|
|
|
|
|
|
|
|
|
### Unknown Property Examples
|
|
|
|
|
|
|
|
|
|
1. Object
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
const PropertyChecker = new Proxy(
|
|
|
|
|
{},
|
|
|
|
|
{
|
|
|
|
|
get(target, propKey, receiver) {
|
|
|
|
|
if (!(propKey in target)) {
|
|
|
|
|
throw new ReferenceError("Unknown property: " + propKey);
|
|
|
|
|
}
|
|
|
|
|
return Reflect.get(target, propKey, receiver);
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const obj = { __proto__: PropertyChecker, foo: 123 };
|
|
|
|
|
// 123
|
|
|
|
|
console.log(obj.foo);
|
|
|
|
|
|
|
|
|
|
// ReferenceError: Unknown property: fo
|
|
|
|
|
obj.fo
|
|
|
|
|
|
|
|
|
|
// [object Object]
|
|
|
|
|
obj.toString()
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. Class
|
|
|
|
|
|
|
|
|
|
If we turn `PropertyChecker` into a constructor, we can use it for
|
|
|
|
|
[classes](20201008090316-class_notation) via extends:
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
function PropertyChecker() {}
|
|
|
|
|
PropertyChecker.prototype = new Proxy(/*···*/);
|
|
|
|
|
|
|
|
|
|
class Point extends PropertyChecker {
|
|
|
|
|
constructor(x, y) {
|
|
|
|
|
super();
|
|
|
|
|
this.x = x;
|
|
|
|
|
this.y = y;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const p = new Point(5, 7);
|
|
|
|
|
console.log(p.x); // 5
|
|
|
|
|
console.log(p.z); // ReferenceError
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Negative Array indices (get)
|
|
|
|
|
|
|
|
|
|
Some [array prototype
|
|
|
|
|
methods](20201009090331-javascript_array_prototype_methods) let you
|
|
|
|
|
refer to the last element via -1, to the second-to-last element via -2,
|
|
|
|
|
etc. For example:
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
console.log(["a", "b", "c"].slice(-1)); // ['c']
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Alas, that doesn’t work when accessing elements via the bracket operator
|
|
|
|
|
(\[\]). We can, however, use proxies to add that capability. The
|
|
|
|
|
following function createArray() creates Arrays that support negative
|
|
|
|
|
indices. It does so by wrapping proxies around Array instances. The
|
|
|
|
|
proxies intercept the get operation that is triggered by the bracket
|
|
|
|
|
operator.
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
function createArray(...elements) {
|
|
|
|
|
const handler = {
|
|
|
|
|
get(target, propKey, receiver) {
|
|
|
|
|
// Sloppy way of checking for negative indices
|
|
|
|
|
const index = Number(propKey);
|
|
|
|
|
if (index < 0) {
|
|
|
|
|
propKey = String(target.length + index);
|
|
|
|
|
}
|
|
|
|
|
return Reflect.get(target, propKey, receiver);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
// Wrap a proxy around an Array
|
|
|
|
|
const target = [];
|
|
|
|
|
target.push(...elements);
|
|
|
|
|
return new Proxy(target, handler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const arr = createArray("a", "b", "c");
|
|
|
|
|
console.log(arr[-1]); // c
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Data binding (set)
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
function createObservedArray(callback) {
|
|
|
|
|
const array = [];
|
|
|
|
|
return new Proxy(array, {
|
|
|
|
|
set(target, propertyKey, value, receiver) {
|
|
|
|
|
callback(propertyKey, value);
|
|
|
|
|
return Reflect.set(target, propertyKey, value, receiver);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const observedArray = createObservedArray(
|
|
|
|
|
(key, value) => console.log(`${key}=${value}`));
|
|
|
|
|
observedArray.push('a');
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Accessing a restful web service
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
function httpGet(url) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const request = new XMLHttpRequest();
|
|
|
|
|
Object.assign(request, {
|
|
|
|
|
onload() {
|
|
|
|
|
if (this.status === 200) {
|
|
|
|
|
// Success
|
|
|
|
|
resolve(this.response);
|
|
|
|
|
} else {
|
|
|
|
|
// Something went wrong (404 etc.)
|
|
|
|
|
reject(new Error(this.statusText));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onerror() {
|
|
|
|
|
reject(new Error("XMLHttpRequest Error: " + this.statusText));
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
request.open("GET", url);
|
|
|
|
|
request.send();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createWebService(baseUrl) {
|
|
|
|
|
return new Proxy(
|
|
|
|
|
{},
|
|
|
|
|
{
|
|
|
|
|
get(target, propKey, receiver) {
|
|
|
|
|
// Return the method to be called
|
|
|
|
|
return () => httpGet(baseUrl + "/" + propKey);
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const service = createWebService("http://example.com/data");
|
|
|
|
|
// Read JSON data in http://example.com/data/employees
|
|
|
|
|
service.employees().then((json) => {
|
|
|
|
|
const employees = JSON.parse(json);
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Recoverable references
|
|
|
|
|
|
|
|
|
|
*Revocable references* work as follows: A client is not allowed to
|
|
|
|
|
access an important resource (an object) directly, only via a reference
|
|
|
|
|
(an intermediate object, a wrapper around the resource). Normally, every
|
|
|
|
|
operation applied to the reference is forwarded to the resource. After
|
|
|
|
|
the client is done, the resource is protected by revoking the reference,
|
|
|
|
|
by switching it off. Henceforth, applying operations to the reference
|
|
|
|
|
throws exceptions and nothing is forwarded, anymore.
|
|
|
|
|
|
|
|
|
|
In the following example, we create a revocable reference for a
|
|
|
|
|
resource. We then read one of the resource’s properties via the
|
|
|
|
|
reference. That works, because the reference grants us access. Next, we
|
|
|
|
|
revoke the reference. Now the reference doesn’t let us read the
|
|
|
|
|
property, anymore.
|
|
|
|
|
|
|
|
|
|
``` javascript
|
|
|
|
|
function createRevocableReference(target) {
|
|
|
|
|
const handler = {}; // forward everything
|
|
|
|
|
const { proxy, revoke } = Proxy.revocable(target, handler);
|
|
|
|
|
return { reference: proxy, revoke };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resource = { x: 11, y: 8 };
|
|
|
|
|
const { reference, revoke } = createRevocableReference(resource);
|
|
|
|
|
|
|
|
|
|
// Access granted
|
|
|
|
|
console.log(reference.x); // 11
|
|
|
|
|
|
|
|
|
|
revoke();
|
|
|
|
|
|
|
|
|
|
// Access denied
|
|
|
|
|
console.log(reference.x); // TypeError: Revoked
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
# See also
|
|
|
|
|
|
|
|
|
|
- [Metaprogramming](20201022095438-javascript_metaprogramming)
|