preface
ES6’s new proxies and reflections give developers the ability to intercept and embed additional behavior into basic actions. Specifically, you can define an associated proxy object for the target object, which can be used as an abstract target object. Various operations on the target object can be controlled in the proxy object before they affect the target object.
Proxy
The Proxy is created using the Proxy constructor. This constructor takes two parameters: the target object and the handler object. Missing any of these parameters raises TypeError.
Creating an empty proxy
As shown in the following code, any operation performed on the proxy object is actually applied to the target object. The only perceptible difference is that the code operates on proxy objects.
const target = { id: 'target' }; const handler = {}; const proxy = new Proxy(target, handler); // The id attribute accesses the same value console.log(target.id); // target console.log(proxy.id); // target // Assigning the target attribute is reflected on both objects because both objects access the same value target.id = 'foo'; // target // Assigning the target attribute is reflected on both objects because both objects access the same value target.id = 'foo'; console.log(target.id); // foo console.log(proxy.id); // foo // Assigning a proxy attribute is reflected on both objects // because the assignment is passed to the target proxy.id = 'bar'; console.log(target.id); // bar console.log(proxy.id); // barCopy the code
Define trap
A catcher can be understood as an “interceptor” defined in a handler object for use directly or indirectly on a proxy object. Each time these basic operations are called on a proxy object, the proxy can intercept and modify the corresponding behavior by calling the catcher function before the operations are propagated to the target object.
const target = { foo: 'bar' }; Const Handler = {// The catcher takes the method name key get() {return 'handler override' in the handler object; }}; const proxy = new Proxy(target, handler); console.log(target.foo); // bar console.log(proxy.foo); // handler overrideCopy the code
The get() trap receives the target object, the property to be queried, and the proxy object. We can modify the above code as follows
const target = { foo: 'bar' }; Const Handler = {// The catcher in the handler object with the method name key get(trapTarget, property, receiver) {console.log(trapTarget === target); console.log(property); console.log(receiver === proxy); return trapTarget[property] } }; const proxy = new Proxy(target, handler); proxy.foo; // true // foo // true console.log(proxy.foo); // bar console.log(target.foo); // barCopy the code
All methods that can be captured in a handler object have corresponding Reflection API methods. These methods have the same name and function signature as the methods intercepted by the catcher, and they also have the same behavior as the intercepted methods. Therefore, it is also possible to define empty proxy objects using the reflection API as follows:
const target = { foo: 'bar' }; Const handler = {get() {// return reflect.get (... arguments); // return reflect.get}}; const proxy = new Proxy(target, handler); console.log(proxy.foo); // bar console.log(target.foo); // barCopy the code
We can also use this to decorate the return value of the property to be accessed.
const target = {
foo: 'bar',
baz: 'qux'
};
const handler = {
get(trapTarget, property, receiver) {
let decoration = '';
if (property === 'foo') {
decoration = ' I love you';
}
return Reflect.get(...arguments) + decoration;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar I love you
console.log(target.foo); // bar
console.log(proxy.baz); // qux
console.log(target.baz); // qux
Copy the code
Revocable agency
Sometimes it may be necessary to break the connection between the proxy object and the target object. For ordinary proxies created using new Proxy(), this connection persists for the lifetime of the Proxy object. Proxy also exposes the Revocable () method, which supports unassociating Proxy objects with target objects. Revoking an agent is irreversible. Furthermore, the revoke function () is idempotent, yielding the same result no matter how many times it is called. Calling the proxy after it has been revoked raises TypeError.
const target = { foo: 'bar' }; const handler = { get() { return 'intercepted'; }}; const { proxy, revoke } = Proxy.revocable(target, handler); console.log(proxy.foo); // intercepted console.log(target.foo); // bar revoke(); console.log(proxy.foo); // TypeErrorCopy the code
Agent another agent
The proxy can intercept the operations of the reflection API, which means it is entirely possible to create a proxy through which to proxy another. This allows you to build multiple layers of interceptors on top of a target object:
const target = {
foo: 'bar'
};
const firstProxy = new Proxy(target, {
get() {
console.log('first proxy');
return Reflect.get(...arguments);
}
});
const secondProxy = new Proxy(firstProxy, {
get() {
console.log('second proxy');
return Reflect.get(...arguments);
}
});
console.log(secondProxy.foo);
// second proxy
// first proxy
// bar
Copy the code
Problems and deficiencies of agency
1. This in the proxy
const target = {
thisValEqualsProxy() {
return this === proxy;
}
}
const proxy = new Proxy(target, {});
console.log(target.thisValEqualsProxy()); // false
console.log(proxy.thisValEqualsProxy()); // true
Copy the code
This doesn’t look like a problem. This points to the caller. But if the target object depends on the object identity, you may encounter unexpected problems.
const wm = new WeakMap();
class User {
constructor(userId) {
wm.set(this, userId);
}
set id(userId) {
wm.set(this, userId);
}
get id() {
return wm.get(this);
}
}
const user = new User(123);
console.log(user.id); // 123
const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); // undefined
Copy the code
This is because the User instance initially uses the target object as the key of a WeakMap, while the proxy object tries to get this instance from itself. To solve this problem, you need to reconfigure the agent, changing the agent User instance to the agent User class itself. After creating an instance of the proxy, the proxy instance will be used as the key of WeakMap:
const UserClassProxy = new Proxy(User, {});
const proxyUser = new UserClassProxy(456);
console.log(proxyUser.id);
Copy the code
2. Agents and internal slots
When proxying Date types: According to the ECMAScript specification, the execution of Date type methods depends on the internal slot [[NumberDate]] on this value. The inner slot does not exist on the proxy object, and the value of the inner slot is not accessible through normal get() and set() operations, so methods that should have been forwarded to the target object after the proxy intercepts raise TypeError:
const target = new Date();
const proxy = new Proxy(target, {});
console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: 'this' is not a Date object
Copy the code
Reflect (reflection)
Reflect objects, like Proxy objects, are a new API provided by ES6 for manipulating objects. Reflect’s design objectives:
- Put some methods of Object that are clearly internal to the language (such as Object.defineProperty) on Reflect.
- Modify the return results of some Object methods to make them more reasonable. For example, Object.defineProperty(obj, name, desc) throws an error if the attribute cannot be defined, while Reflect.defineProperty(obj, name, desc) returns false.
- Make all Object operations functions. Some Object operations are imperative, such as name in obj and delete obj[name], while reflect.has (obj, name) and reflect.deleteProperty (obj, name) make them functional behavior.
- The Reflect method corresponds to the Proxy method. As long as it is a Proxy method, the corresponding method can be found on the Reflect object. This allows the Proxy object to easily call the corresponding Reflect method, completing the default behavior as a basis for modifying the behavior. That is, no matter how the Proxy changes the default behavior, you can always get the default behavior in Reflect.
Proxy and reflection APIS
get()
Receiving parameters:
- Target: indicates the target object.
- Property: String key property on the referenced target object.
- Receiver: Proxy object or object that inherits proxy object.
Returns:
- Return value unlimited
The GET () trap is called during the operation to get the property value. The corresponding reflection API method is reflect.get ().
const myTarget = {}; const proxy = new Proxy(myTarget, { get(target, property, receiver) { console.log('get()'); return Reflect.get(... arguments) } }); proxy.foo; // get()Copy the code
set()
Receiving parameters:
- Target: indicates the target object.
- Property: String key property on the referenced target object.
- Value: The value to be assigned to the attribute.
- Receiver: The object that receives the initial assignment.
Returns:
- Return true to indicate success; Returning false indicates failure, and TypeError is raised in strict mode.
The set() trap is called during an operation to set the property value. The corresponding reflection API method is reflect.set ().
const myTarget = {}; const proxy = new Proxy(myTarget, { set(target, property, value, receiver) { console.log('set()'); return Reflect.set(... arguments) } }); proxy.foo = 'bar'; // set()Copy the code
has()
Receiving parameters:
- Target: indicates the target object.
- Property: String key property on the referenced target object.
Returns:
- Has () must return a Boolean value indicating whether the attribute exists. Returns non-Boolean values are converted to booleans.
The HAS () trap is called in the IN operator. The corresponding reflection API method is reflect.has ().
const myTarget = {}; const proxy = new Proxy(myTarget, { has(target, property) { console.log('has()'); return Reflect.has(... arguments) } }); 'foo' in proxy; // has()Copy the code
defineProperty()
The reflect.defineProperty method is basically equivalent to object.defineProperty and is used to define attributes for an Object.
Receiving parameters:
- Target: indicates the target object.
- Property: String key property on the referenced target object.
- Descriptor: Contains optional enumerable, 64x, writable, value, get, and set definitions.
Returns:
- DefineProperty () must return a Boolean value indicating whether the attribute was successfully defined. Returns non-Boolean values are converted to booleans.
const myTarget = {}; const proxy = new Proxy(myTarget, { defineProperty(target, property, descriptor) { console.log('defineProperty()'); return Reflect.defineProperty(... arguments) } }); Object.defineProperty(proxy, 'foo', { value: 'bar' }); // defineProperty()Copy the code
getOwnPropertyDescriptor()
Reflect. GetOwnPropertyDescriptor basic equivalent to the Object. GetOwnPropertyDescriptor, used to get the description of the specified attribute Object.
Receiving parameters:
- Target: indicates the target object.
- Property: String key property on the referenced target object.
Returns:
- GetOwnPropertyDescriptor () must return the object, or undefined if the property does not exist.
const myTarget = {}; const proxy = new Proxy(myTarget, { getOwnPropertyDescriptor(target, property) { console.log('getOwnPropertyDescriptor()'); return Reflect.getOwnPropertyDescriptor(... arguments) } }); Object.getOwnPropertyDescriptor(proxy, 'foo'); // getOwnPropertyDescriptor()Copy the code
deleteProperty()
The reflect. deleteProperty method is equivalent to delete obj[name], used to delete an object’s property.
Receiving parameters:
- Target: indicates the target object.
- Property: String key property on the referenced target object.
Returns:
- DeleteProperty () must return a Boolean value indicating whether the property was successfully deleted. Returns non-Boolean values are converted to booleans.
ownKeys()
Reflect. OwnKeys method is used to return all attributes of the Object, basic is equal to the Object. The getOwnPropertyNames and Object. The getOwnPropertySymbols combined.
Receiving parameters:
- Target: indicates the target object.
Returns:
- OwnKeys () must return an enumerable object containing a string or symbol.
getPrototypeOf()
The reflect.getPrototypeof method is used to read the __proto__ property of an object
Receiving parameters:
- Target: indicates the target object.
Returns:
- GetPrototypeOf () must return an object or NULL.
And so on.
The proxy pattern
Trace property access
By capturing operations such as GET, SET, and HAS, you can know when an object property is accessed and queried. Putting an object proxy that implements the corresponding catcher into your application allows you to monitor when and where the object has been accessed:
const user = { name: 'Jake' }; const proxy = new Proxy(user, { get(target, property, receiver) { console.log(`Getting ${property}`); return Reflect.get(... arguments); }, set(target, property, value, receiver) { console.log(`Setting ${property}=${value}`); return Reflect.set(... arguments); }}); proxy.name; // Getting name proxy.age = 27; // Setting age=27Copy the code
Hidden attribute
The internal implementation of the agent is not visible to external code, so it is easy to hide properties on the target object.
const hiddenProperties = ['foo', 'bar'];
const targetObject = {
foo: 1,
bar: 2,
baz: 3
};
const proxy = new Proxy(targetObject, {
get(target, property) {
if (hiddenProperties.includes(property)) {
return undefined;
} else {
return Reflect.get(...arguments);
}
},
has(target, property) {
if (hiddenProperties.includes(property)) {
return false;
} else {
return Reflect.has(...arguments);
}
}
});
// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log('foo' in proxy); // false
console.log('bar' in proxy); // false
console.log('baz' in proxy); // true
Copy the code
Property verification
Because all assignments trigger the set() trap, assignments can be allowed or denied depending on the value assigned:
const target = { onlyNumbersGoHere: 0 }; const proxy = new Proxy(target, { set(target, property, value) { if (typeof value ! == 'number') { return false; } else { return Reflect.set(... arguments); }}}); proxy.onlyNumbersGoHere = 1; console.log(proxy.onlyNumbersGoHere); // 1 proxy.onlyNumbersGoHere = '2'; console.log(proxy.onlyNumbersGoHere); / / 1Copy the code
Function and constructor parameter validation
Similar to protecting and validating object properties, function and constructor arguments can also be reviewed. For example, you can make a function accept only certain types of values:
function median(... nums) { return nums.sort()[Math.floor(nums.length / 2)]; } const proxy = new Proxy(median, { apply(target, thisArg, argumentsList) { for (const arg of argumentsList) { if (typeof arg ! == 'number') { throw 'Non-number argument provided'; } } return Reflect.apply(... arguments); }}); console.log(proxy(4, 7, 1)); // 4 console.log(proxy(4, '7', 1)); // Error: non-number argument provided similarly, you can require that instantiation must take arguments to the constructor: class User {constructor(id) {this.id_ = id; } } const proxy = new Proxy(User, { construct(target, argumentsList, newTarget) { if (argumentsList[0] === undefined) { throw 'User cannot be instantiated without id'; } else { return Reflect.construct(... arguments); }}}); new proxy(1); new proxy(); // Error: User cannot be instantiated without idCopy the code
Data binding and observables
A proxy lets you link together parts of the runtime that would otherwise be unrelated. This allows you to implement patterns that allow different code to interoperate. For example, you can bind the proxied class to a global instance collection and have all created instances added to the collection:
const userList = [];
class User {
constructor(name) {
this.name_ = name;
}
}
const proxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments);
userList.push(newUser);
return newUser;
}
});
new proxy('John');
new proxy('Jacob');
new proxy('Jingleheimerschmidt');
console.log(userList); // [User {}, User {}, User{}]
Copy the code
Alternatively, you can bind the collection to an event dispatcher that sends a message each time a new instance is inserted:
const userList = [];
function emit(newValue) {
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments);
if (result) {
emit(Reflect.get(target, property, receiver));
}
return result;
}
});
proxy.push('John');
// John
proxy.push('Jacob');
// Jacob
Copy the code
Use Proxy to implement the observer pattern
const queuedObservers = new Set(); const observe = fn => queuedObservers.add(fn); const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); queuedObservers.forEach(observer => observer()); return result; } const person = Observable ({name: 'observable ', age: 20}); function print() { console.log(`${person.name}, ${person.age}`) } observe(print); Person. Name = 'person '; // output // Li Si, 20Copy the code
At the end
This article mainly refers to ruan Yifeng ES6 tutorial, JS Little Red Book fourth edition
Because my level is limited, if there is any mistake, please contact me to point out, thank you.