Proxy

A property descriptor encodes the attributes of a property as a JavaScript object. Their TypeScript interfaces look as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
interface DataPropertyDescriptor {
value?: any;
writable?: boolean;
configurable?: boolean;
enumerable?: boolean;
}
interface AccessorPropertyDescriptor {
get?: (this: any) => any;
set?: (this: any, v: any) => void;
configurable?: boolean;
enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;
Proxy 支持的拦截操作 13 种
  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
Function-specific traps

If the target is a function, two additional operations can be intercepted:

  • apply: Making a function call. Triggered via:

    • proxy(···)
    • proxy.call(···)
    • proxy.apply(···)
  • construct: Making a constructor call. Triggered via:

    • new proxy(···)
Intercepting method calls

If we want to intercept method calls via a Proxy, we are facing a challenge: There is no trap for method calls. Instead, a method call is viewed as a sequence of two operations:

  • A get to retrieve a function
  • An apply to call that function

Therefore, if we want to intercept method calls, we need to intercept two operations:

  • First, we intercept the get and return a function.
  • Second, we intercept the invocation of that function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const traced = [];

function traceMethodCalls(obj) {
const handler = {
get(target, propKey, receiver) {
const origMethod = target[propKey];
// 这里 this 指向 proxy
return function (...args) { // implicit parameter `this`!
// 这里 this 指向 target
const result = origMethod.apply(this, args);
traced.push(propKey + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
}
};
return new Proxy(obj, handler);
}

const obj = {
multiply(x, y) {
return x * y;
},
squared(x) {
return this.multiply(x, x);
},
};

const tracedObj = traceMethodCalls(obj);
console.log(tracedObj.squared(9)); // 81
console.log(traced); // [ 'multiply[9,9] -> 81', 'squared[9] -> 81' ]
Revocable Proxies 可废除的代理
1
2
3
4
5
6
7
8
9
10
11
const target = {}; // Start with an empty object
const handler = {}; // Don’t intercept anything
const {proxy, revoke} = Proxy.revocable(target, handler);

// `proxy` works as if it were the object `target`:
proxy.city = 'Paris';
console.log(proxy.city); // Paris

revoke();

console.log(proxy.prop); // TypeError: Cannot perform 'get' on a proxy that has been revoked
Proxies as prototypes
1
2
3
4
5
6
7
8
9
10
11
12
const proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});

const obj = Object.create(proto);
obj.weight;

// Output:
// 'GET weight'
Forwarding intercepted operations
1
2
3
4
5
6
7
8
9
10
11
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return delete target[propKey];
},
has(target, propKey) {
console.log('HAS ' + propKey);
return propKey in target;
},
// Other traps: similar
}
1
2
3
4
5
6
7
8
9
10
11
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return Reflect.deleteProperty(target, propKey);
},
has(target, propKey) {
console.log('HAS ' + propKey);
return Reflect.has(target, propKey);
},
// Other traps: similar
}
1
2
3
4
5
6
7
8
9
10
const handler = new Proxy({}, {
get(target, trapName, receiver) {
// Return the handler method named trapName
return (...args) => {
console.log(trapName.toUpperCase() + ' ' + args);
// Forward the operation
return Reflect[trapName](...args);
};
},
});
proxy affects this
1
2
3
4
5
6
7
8
9
10
11
12
13
const target = {
myMethod() {
return {
thisIsTarget: this === target,
thisIsProxy: this === proxy,
};
}
};
const handler = {};
const proxy = new Proxy(target, handler);

console.log(target.myMethod()); // { thisIsTarget: true, thisIsProxy: false }
console.log(proxy.myMethod()); // { thisIsTarget: false, thisIsProxy: true }
Objects that can’t be wrapped transparently
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}

const jane = new Person('Jane');
console.log(jane.name); // Jane

const proxy = new Proxy(jane, {});
console.log(proxy.name); // undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person2 {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}

const jane2 = new Person2('Jane');
console.log(jane2.name); // Jane

const proxy2 = new Proxy(jane2, {});
console.log(proxy2.name); // Jane
Wrapping instances of built-in constructors

Instances of most built-in constructors also use a mechanism that is not intercepted by Proxies. They therefore can’t be wrapped transparently

1
2
3
4
5
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

console.log(proxy.getFullYear()); // TypeError: this is not a Date object.
1
2
3
4
5
6
7
8
9
10
const handler = {
get(target, propKey, receiver) {
if (propKey === 'getFullYear') {
return target.getFullYear.bind(target);
}
return Reflect.get(target, propKey, receiver);
},
};
const proxy = new Proxy(new Date('2030-12-24'), handler);
console.log(proxy.getFullYear()); // 2030
Arrays can be wrapped transparently

Use cases for Proxies

Tracing property accesses (get, set)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `Point(${this.x}, ${this.y})`;
}
}

function tracePropertyAccesses(target, propKeys) {
return new Proxy(target, {
get(target, propName, receiver) {
if(propKeys.find(e => e === propName)) {
console.log("GET "+propName);
}
Reflect.get(target, propName, receiver);
},
set(target, propName, propValue, receiver) {
if(propKeys.find(e => e === propName)) {
console.log("SET "+propName+"="+propValue);
}
Reflect.set(target, propName, receiver);
}
});
}

// Trace accesses to properties `x` and `y`
const point = new Point(5, 7);
const tracedPoint = tracePropertyAccesses(point, ['x', 'y']);

console.log(tracedPoint.x)
tracedPoint.y = 10;
tracedPoint.z = 15;
tracedPoint.x = 8;

/*
'GET x'
'SET y=10'
'SET x=8'
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const propertyCheckerHandler = {
get(target, propKey, receiver) {
// Only check string property keys
if (typeof propKey === 'string' && !(propKey in target)) {
throw new ReferenceError('Unknown property: ' + propKey);
}
return Reflect.get(target, propKey, receiver);
}
};
const PropertyChecker = new Proxy({}, propertyCheckerHandler);

let jane = Object.create(PropertyChecker);
jane.name = "Jane";

console.log(jane.name); // Jane
console.log(jane.nmea); // ReferenceError: Unknown property: nmea
console.log(jane.toString()); // [object Object]

Negative Array indices (get)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createArray(...elements) {
const handler = {
get(target, propKey, receiver) {
if (typeof propKey === 'string') {
const index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
}
return Reflect.get(target, propKey, receiver);
}
};
// Wrap a proxy around the Array
return new Proxy(elements, handler);
}
const arr = createArray('a', 'b', 'c');
console.log(arr[-1]); // c
console.log(arr[0]); // a
console.log(arr.length); // 3

Data binding (set)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(
`${JSON.stringify(key)} = ${JSON.stringify(value)}`));
observedArray.push('a');

// Output:
// '"0" = "a"'
// '"length" = 1'

Accessing a restful web service (method calls)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
// Return the method to be called
return () => httpGet(baseUrl + '/' + propKey);
}
});
}

function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}

const service = createWebService('http://example.com/data');
// Read JSON data in http://example.com/data/employees
service.employees().then((jsonStr) => {
const employees = JSON.parse(jsonStr);
// ···
});

Revocable 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createRevocableReference(target) {
let enabled = true;
return {
reference: new Proxy(target, {
get(target, propKey, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'get' on a proxy that has been revoked`);
}
return Reflect.get(target, propKey, receiver);
},
has(target, propKey) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'has' on a proxy that has been revoked`);
}
return Reflect.has(target, propKey);
},
// (Remaining methods omitted)
}),
revoke: () => {
enabled = false;
},
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createRevocableReference(target) {
let enabled = true;
const handler = new Proxy({}, {
get(_handlerTarget, trapName, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform '${trapName}' on a proxy`
+ ` that has been revoked`);
}
return Reflect[trapName];
}
});
return {
reference: new Proxy(target, handler),
revoke: () => {
enabled = false;
},
};
}
1
2
3
4
5
function createRevocableReference(target) {
const handler = {}; // forward everything
const { proxy, revoke } = Proxy.revocable(target, handler);
return { reference: proxy, revoke };
}

Membranes

Membranes build on the idea of revocable references: Libraries for safely running untrusted code wrap a membrane around that code to isolate it and to keep the rest of the system safe. Objects pass the membrane in two directions:

  • The untrusted code may receive objects (“dry objects”) from the outside.
  • Or it may hand objects (“wet objects”) to the outside.

In both cases, revocable references are wrapped around the objects. Objects returned by wrapped functions or methods are also wrapped. Additionally, if a wrapped wet object is passed back into a membrane, it is unwrapped.

Once the untrusted code is done, all of the revocable references are revoked. As a result, none of its code on the outside can be executed anymore and outside objects that it references, cease to work as well. The Caja Compiler is “a tool for making third party HTML, CSS and JavaScript safe to embed in your website”. It uses membranes to achieve this goal.

Proxies are stratified: Base level (the Proxy object) and meta level (the handler object) are separate

Proxies are used in two roles:

  • As wrappers, they wrap their targets, they control access to them. Examples of wrappers are: revocable resources and tracing via Proxies.

  • As virtual objects, they are simply objects with special behavior and their targets don’t matter. An example is a Proxy that forwards method calls to a remote object.

Proxies are shielded in two ways:

  • It is impossible to determine whether an object is a Proxy or not (transparent virtualization).
  • We can’t access a handler via its Proxy (handler encapsulation).
tell Proxies apart from non-Proxies
1
2
3
4
5
6
7
8
9
10
11
12
const proxies = new WeakSet();

function createProxy(obj) {
const handler = {};
const proxy = new Proxy(obj, handler);
proxies.add(proxy);
return proxy;
}

function isProxy(obj) {
return proxies.has(obj);
}
参考链接
  1. https://exploringjs.com/deep-js/ch_proxies.html