1. Introduction
new Proxy(target, handler) syntax, allows handler object handler to intercept target object target's property access methods
Proxy mechanism supports rewriting 14 internal methods of objects, such as [[Get]](key, receiver), [[Set]](key, value, receiver), etc., where receiver is the object where search starts first (the property to access may be on prototype chain)
First create a proxy:
var obj = {
key: 'value'
};
var handler = {
get: function(target, key, receiver) {
console.log(`proxy key = ${key}`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(`proxy key = ${key}, value = ${value}`);
return Reflect.set(target, key, value, receiver);
}
// , apply
// , construct
// , defineProperty
// , deleteProperty
// , enumerate
// , getOwnPropertyDescriptor
// , getPrototypeOf
// , has
// , isExtensible
// , ownKeys
// , preventExtensions
// , setPrototypeOf
};
var proxy = new Proxy(obj, handler);
Then try it out:
// test
// Copy initial state from target object obj
console.log(proxy); // Object { key: "value" }
proxy.a = 1; // proxy key = a, value = 1
proxy['b'] = 2; // proxy key = b, value = 2
proxy.b++;
// log print:
// proxy key = b
// proxy key = b, value = 3
// Proxy object proxy's state is completely consistent with target object proxy
// Because all internal methods are forwarded
console.log(`proxy.key = ${proxy.key}, proxy.a = ${proxy.a}, proxy.b = ${proxy.b}`);
console.log(`obj.key = ${obj.key}, obj.a = ${obj.a}, obj.b = ${obj.b}`);
// log print:
// proxy key = key
// proxy key = a
// proxy key = b
// proxy.key = value, proxy.a = 1, proxy.b = 3
// obj.key = value, obj.a = 1, obj.b = 3
// Reverse
obj.x = 2;
console.log(`obj.x = ${obj.x}, proxy.x = ${proxy.x}`);
// log print:
// proxy key = x
// obj.x = 2, proxy.x = 2
Just like copying reference (var proxy = obj;), obj and proxy states are completely consistent, bidirectional synchronization, the only difference is handler secretly did some things
2. Characteristics
1. Behavior Forwarding and State Synchronization
Proxy behavior: Forward all proxied internal methods to target
Proxy object proxy and target object target maintain consistent state: Copy current state from target when creating proxy object, internal methods are all forwarded
2. proxy !== target
For example target is DOMElement, but proxy is not, calling document.body.appendChild(proxy) will trigger TypeError
Moreover, in above example
console.log(obj === proxy); // false
console.log(obj == proxy); // false
This is reasonable, because proxy is inherently different from reference copying
3. handler Interception
Can rewrite 14 internal methods through handler object, intercept and change target's default behavior
Internal methods not intercepted by handler (not rewritten in handler object) will directly point to target
All 14 internal methods can be rewritten in handler object, property names are consistent with 14 property names of Reflect object, details MDN
4. Proxy Relationship Can Be Released
Proxy can be released, use Proxy.revocable(target, handler) to create return Object { proxy: Object, revoke: revoke() }, call revoke() to release proxy relationship, after release accessing proxy object will error, for example:
// Release proxy relationship
var rProxy = Proxy.revocable({}, {
set: function(target, key, value, receiver) {
return Reflect.set(target, key, 0, receiver);
}
});
var p = rProxy.proxy;
p.a = 123;
console.log(p.a); // 0
// Release proxy
rProxy.revoke();
// p.b = 213; // Error TypeError: illegal operation attempted on a revoked proxy
5. Object Invariance
Unless target is non-extensible, otherwise proxy object cannot be declared as non-extensible
3. Application Scenarios
1. Auto-Fill Objects
// 1. Auto-fill
var Tree = function() {
return new Proxy({}, {
get: function(target, key, receiver) {
if (typeof target[key] === 'undefined') {
console.log(`${key} is undef, but it will be auto filled`);
// Use current handler to create proxy as value
Reflect.set(target, key, new Proxy({}, this), receiver);
// Above code equivalent to
// target[key] = new Proxy({}, this);
// Also equivalent to
// target[key] = Tree();
}
return Reflect.get(target, key, receiver);
}
});
}
var tree = Tree();
console.log(tree); // Object { branch1: Object }
tree.branch1.branch2.twig = 'green';
// log print:
// branch1 is undef, but it will be auto filled
// branch2 is undef, but it will be auto filled
console.log(tree); // Object { branch1: Object }
tree.branch1.branch3.twig = "yellow";
// log print:
// branch3 is undef, but it will be auto filled
console.log(tree); // Object { branch1: Object }
Regardless of whether it's useful, but in pre-ES6 era truly impossible to implement auto-fill objects (except "compiling" source code black magic)
P.S. Above log results from Firefox47, because Chrome51.0.2704.106 showed weird things:
// chrome log print:
Object {splice: Object}
splice is undef, but it will be auto filled
splice is undef, but it will be auto filled
branch1 is undef, but it will be auto filled
branch2 is undef, but it will be auto filled
Object {splice: Object, branch1: Object}
splice is undef, but it will be auto filled
branch3 is undef, but it will be auto filled
Object {splice: Object, branch1: Object}
Where did splice come from? Moreover checking splice value Object, will find can expand infinitely (console will display a very long wide > shape), feels like splice circularly references itself. Let's assume it's Chrome implementation bug, no need to delve deeper
2. Tamper-Proof Objects
Creating proxies requires great caution, otherwise there are vulnerabilities
function readOnly(obj) {
var err = new Error('can\'t modify read-only object');
return new Proxy(obj, {
// 1. Rewrite all setters
set: function(target, key, value, receiver) {
throw err;
},
IsExtensible: function(target, receiver) {
return false;
},
deleteProperty: function(target, key, receiver) {
throw err;
},
defineProperty: function(target, key, desc, receiver) {
throw err;
},
setPrototypeOf: function(target, proto, receiver) {
throw err;
},
// 2. Rewrite all getters
// getter has vulnerabilities, x is read-only, but x.prop is still writable
// Tampering with x.prop may affect x internal logic
// So also need to rewrite get
get: function(target, key, receiver) {
var res = Reflect.get(target, key, receiver);
// if (typeof res === 'object') {
if (Object(res) === res) { // Check if res is Object type
res = readOnly(res);
}
return res;
},
getPrototypeOf: this.get,
getOwnPropertyDescriptor: this.get
});
}
var openObj = {
attr: 1,
obj: {
val: 1,
getVal: function() {
return this.val;
}
},
fn: function() {
console.log('fn is called');
}
};
var closeObj = readOnly(openObj);
// Avoid throw err interrupting execution flow, affecting tests
var nextTick = fn => setTimeout(fn, 50);
nextTick(() => console.log(closeObj.attr));
nextTick(() => closeObj.fn());
nextTick(() => closeObj.fn = console.log);
nextTick(() => delete closeObj.obj);
// If forget to limit getter will appear bug
nextTick(() => {
closeObj.obj.val = 'hack';
console.log(closeObj.obj.getVal()); // hack
})
readOnly implementation is unimaginably long, does a tamper-proof object need to consider so much? Yes. And neglecting any point will trigger vulnerabilities, rewriting default behavior with proxy mechanism requires great caution. Above solution still has problems:
// Access 2 times, new 2 proxy objects
console.log(closeObj.obj === closeObj.obj); // false
Caching proxy can avoid creating unnecessary proxy objects, simplest way to cache Object is of course [WeakMap](/articles/集合(set 和 map)-es6 笔记 8/), as follows:
function readOnlyEx(obj) {
var proxyCache = new WeakMap();
//...
if (Object(res) === res) { // Check if res is Object type
// Cache
if (!proxyCache.get(res)) {
proxyCache.set(res, readOnly(res));
}
res = proxyCache.get(res);
}
//...
}
// test
var _closeObj = readOnlyEx(openObj);
// Same proxy object, because of WeakMap
console.log(_closeObj.obj === _closeObj.obj); // true
Additionally, proxy !== target is a problem needing attention, for example new Proxy(window, {}).alert(1) errors cannot execute, needs apply handling, as follows:
// proxy !== target
// new Proxy(window, {}).alert(1);
// Will error TypeError: 'alert' called on an object that does not implement interface Window.
var mWin = new Proxy(window, {
get: function(target, key, receiver) {
var res = Reflect.get(target, key, receiver);
//! function judgment may not be strict enough
if (typeof res === 'function') {
return function() {
return res.apply(target, arguments);
}
}
}
});
// test
mWin.alert(123); // alert 123
3. Separate Validation Logic
setter/getter advantages, through proxy mechanism can embed validation logic into property access behavior (validation logic has place to put)
4. Record Object Access
Mainly used in testing/debugging, such as testing frameworks, through proxy mechanism can record everything throughout the process
5. Enhance Ordinary Objects
Such as auto-fill objects and tamper-proof objects, has great imagination space in API design aspects
6. Cooperate with WeakMap
Avoid creating same proxy multiple times, such as readOnlyEx, using WeakMap to manage proxy objects is most natural thing
4. Object's Internal Methods
Translated from: http://www.ecma-international.org/ecma-262/6.0/index.html#table-5
Table 5 — Required Internal Methods| Internal Method | Signature | Description | Explanation |
|---|---|---|---|
| [[GetPrototypeOf]] | () → Object | Null | Determine the object that provides inherited properties for current object, null value indicates no inherited properties | Called when executing obj.__proto__ or obj['__proto__'] or Object.getPrototypeOf(obj) |
| [[SetPrototypeOf]] | (Object | Null) → Boolean | Establish association between current object and another object that provides inherited properties, passing null indicates no need to inherit properties, returning true indicates operation completed successfully, false indicates operation failed | |
| [[IsExtensible]] | ( ) → Boolean | Decide whether to allow adding extra properties to current object | |
| [[PreventExtensions]] | ( ) → Boolean | Control whether new properties can be added to current object, return true if operation successful, otherwise return false | |
| [[GetOwnProperty]] | (propertyKey) → Undefined | Property Descriptor | If current object owns property named propertyKey, return corresponding Property Descriptor for the own property of this object whose key is , otherwise return undefined | |
| [[HasProperty]] | (propertyKey) → Boolean | Return a boolean value indicating whether current object already owns property named propertyKey (the property can be current object's or inherited) | Called when executing key in object |
| [[Get]] | (propertyKey, Receiver) → any | Return the value of property named propertyKey of current object, if need to execute ES code to retrieve this property value (from prototype chain), will use Receiver as this value | Called when executing obj.attr or obj['attr'] |
| [[Set]] | (propertyKey,value, Receiver) → Boolean | Set property value named propertyKey to value, if need to execute ES code to set this property value (property comes from prototype chain), will use Receiver as this value. Return true if property value set successfully, otherwise return false | Called when executing obj.attr/obj[attr] = val, when executing +=,++,-- first call [[Get]] then call [[Set]] |
| [[Delete]] | (propertyKey) → Boolean | Remove property named propertyKey from current object, return false if property not deleted still exists, return true if property deleted or doesn't exist | |
| [[DefineOwnProperty]] | (propertyKey, PropertyDescriptor) → Boolean | Create or modify existing property named propertyKey, set to state described by PropertyDescriptor. Return true if property successfully created/updated, otherwise return false | |
| [[Enumerate]] | ()→Object | Return iterator object that can generate enumerable property key set of current object | Called when executing for (var key in obj), internal method returns an iterable object, for...in loop can obtain object key set through this internal method |
| [[OwnPropertyKeys]] | ()→List of propertyKey | Return a List composed of keys of all owned properties of current object (excluding properties from prototype chain) |
| Internal Method | Signature | Description | Explanation |
|---|---|---|---|
| [[Call]] | (any, a List of any) → any | Execute relevant code of current object, called through function call expression. Internal method's parameters are this value and parameter list passed in through function call expression. Objects implementing this internal method are callable. | Called when executing fn() or obj.fn() |
| [[Construct]] | (a List of any, Object) → Object | Create an object, called through new or super operator. Internal method's first parameter is operator's parameter list, second parameter is object where new operator was initially applied. Objects implementing this internal method are called constructors, function objects are not necessarily constructors, such non-constructor function objects don't have [[Construct]] internal method | Called when executing new Type() or super() |
P.S. super keyword used to call parent class constructor (super(args)) or access parent class properties (super.attr/super.fn(args), cooperate with class keyword to define classes, details see super)
5. Summary
js has built-in proxy mechanism,配合 reflection mechanism (although very weak) to be taken with warm water
Could there be annotations? Looking at it this way, js automatic testing robot has some hope
References
- "ES6 in Depth": Free e-book provided by InfoQ Chinese site
No comments yet. Be the first to share your thoughts.