Preface
ES6's new proxy and
reflection provide developers with the ability to intercept and embed additional behaviors into the
. Specifically, an associated proxy object can be defined for the target object, and this proxy object can be used as an abstract target object. Before various operations on the target object affect the target object, these operations can be controlled in the proxy object.
Proxy
The proxy is Proxy
constructor. This constructor receives two parameters: the target object and the handler object. Missing any of these parameters will throw a TypeError.
Create an empty proxy
As shown in the code below, any operation performed on the proxy object will actually be applied to the target object. The only perceivable difference
That is, the operation in the code is a proxy object.
const target = {
id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar
Define catcher
The catcher can be understood as a kind of "interceptor" defined in the handler object and used directly or indirectly on the proxy object. Every time these basic operations are called on the proxy object, the proxy can propagate these operations to the target object Before calling the catcher function, intercept and modify the corresponding behavior.
const target = {
foo: 'bar'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return 'handler override';
}
};
const proxy = new Proxy(target, handler);
console.log(target.foo); // bar
console.log(proxy.foo); // handler override
get()
will receive the target object, the attributes to be queried and the three parameters of the proxy object. We can modify the above code as follows
const target = {
foo: 'bar'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
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); // bar
All the methods that can be captured in the handler object have corresponding Reflect API methods. These methods have the same names and function signatures as the methods intercepted by the catcher, and they also have the same behavior as the intercepted methods. Therefore, using the reflection API can also define an empty proxy object 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); // bar
We can also use this to modify return value of the attribute 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
Revocable proxy
Sometimes it may be necessary to interrupt the connection between the proxy object and the target object. For ordinary proxies created using new Proxy(), this connection will persist throughout the life cycle of the proxy object. Proxy also exposes the revocable()
method, which supports the cancellation of the association between the proxy object and the target object. The operation of revoking the proxy is irreversible. Moreover, the undo function ( revoke()
) is idempotent, and the result is the same how many times it is called. Calling the proxy after the proxy is cancelled will throw a 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); // TypeError
Agent another agent
The proxy can intercept the operation of the reflection API, and this means that it is possible to create a proxy and use it to proxy another proxy. In this way, a multi-layer interception network can be built 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
Problems and deficiencies of agency
1. This in the agent
const target = {
thisValEqualsProxy() {
return this === proxy;
}
}
const proxy = new Proxy(target, {});
console.log(target.thisValEqualsProxy()); // false
console.log(proxy.thisValEqualsProxy()); // true
It seems that there is no problem, this points to the caller. But if the target object relies on the object identity, it 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
This is because the User instance uses the target object as the key of the WeakMap at the beginning, but the proxy object tries to obtain this reality from itself.
example. To solve this problem, you need to reconfigure the proxy and change the proxy User instance to the proxy User class itself. Create a code later
The management instance will use the proxy instance as the WeakMap key:
const UserClassProxy = new Proxy(User, {});
const proxyUser = new UserClassProxy(456);
console.log(proxyUser.id);
2. Proxy and internal slots
When proxying the Date type: According to the ECMAScript specification, the execution of the Date type method depends on the internal slot [[NumberDate]] on the this value. This internal slot does not exist on the proxy object, and the value of this internal slot cannot be accessed through ordinary get() and set() operations, so the method that should be forwarded to the target object after the proxy intercepts will throw a 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
Reflect
Reflect
object, like the Proxy
object, is also a new API provided by ES6 to manipulate objects. The design purpose of Reflect:
- Put some methods (such as Object.defineProperty) of the Object object that are obviously internal to the language on the Reflect object.
- Modify the return result of some Object methods to make it more reasonable. For example, Object.defineProperty(obj, name, desc) will throw an error when the property cannot be defined, while Reflect.defineProperty(obj, name, desc) will return false.
- Let Object operations become functional behaviors. Certain 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 behaviors.
- The methods of the Reflect object have a one-to-one correspondence with the methods of the Proxy object. As long as it is a method of the Proxy object, the corresponding method can be found on the Reflect object. This allows the Proxy object to conveniently call the corresponding Reflect method to complete the default behavior as the basis for modifying the behavior. In other words, no matter how Proxy modifies the default behavior, you can always get the default behavior on Reflect.
Proxy and reflection API
get()
Receive parameters:
- target: the target object.
- property: The string key property on the referenced target object.
- receiver: The proxy object or the object that inherits the proxy object.
return:
- Unlimited return value
The get() catcher will be called in the operation to get the attribute 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()
set()
Receive parameters:
- target: the target object.
- property: The string key property on the referenced target object.
- value: The value to be assigned to the attribute.
- receiver: Receive the initially assigned object.
return:
- Return true to indicate success; return false to indicate failure, and TypeError will be thrown in strict mode.
The set() catcher will be called during the operation of setting 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()
has()
Receive parameters:
- target: the target object.
- property: The string key property on the referenced target object.
return:
- has() must return a boolean value, indicating whether the property exists. Returning a non-boolean value will be converted to a boolean value.
The has() catcher will be 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()
defineProperty()
The Reflect.defineProperty method is basically equivalent to Object.defineProperty, which is used to define properties for objects.
Receive parameters:
- target: the target object.
- property: The string key property on the referenced target object.
- descriptor: contains optional enumerable, configurable, writable, value, get and set defined objects.
return:
- defineProperty() must return a boolean value, indicating whether the property is successfully defined. Returning a non-boolean value will be converted to a boolean value.
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()
getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor is basically equivalent to Object.getOwnPropertyDescriptor, used to get the description object of the specified property.
Receive parameters:
- target: the target object.
- property: The string key property on the referenced target object.
return:
- 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()
deleteProperty()
The Reflect.deleteProperty method is equivalent to delete obj[name] and is used to delete the properties of an object.
Receive parameters:
- target: the target object.
- property: The string key property on the referenced target object.
return:
- deleteProperty() must return a boolean value, indicating whether the deletion of the property is successful. Returning a non-boolean value will be converted to a boolean value.
ownKeys()
The Reflect.ownKeys method is used to return all the properties of the object, which is basically equivalent to the sum of Object.getOwnPropertyNames and Object.getOwnPropertySymbols.
Receive parameters:
- target: the target object.
return:
- ownKeys() must return an enumerable object containing a string or symbol.
getPrototypeOf()
The Reflect.getPrototypeOf method is used to read the __proto__ property of the object
Receive parameters:
- target: the target object.
return:
- getPrototypeOf() must return an object or null.
and many more. .
Agency model
Track attribute access
By capturing operations such as get, set, and has, you can know when the object properties are accessed and queried. Put an object proxy that implements the corresponding catcher into the application, and you can monitor when and where this 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=27
Hidden attributes
The internal implementation of the agent is invisible to external code, so it is easy to hide the attributes 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
Attribute validation
Because all assignment operations trigger the set() catcher, you can decide whether to allow or deny assignment based on the assigned value:
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); // 1
Function and constructor parameter verification
Similar to protecting and verifying object properties, function and constructor parameters can also be reviewed. For example, you can make a function only accept 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
类似地,可以要求实例化时必须给构造函数传参:
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 id
Data binding and observable objects
Through the proxy, the originally unrelated parts of the runtime can be linked together. In this way, various modes can be implemented, allowing different codes to interoperate. For example, you can bind the proxied class to a global instance collection, so that all created instances are added to this 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{}]
In addition, you can bind the collection to an event dispatcher, which sends a message every 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
Use Proxy to implement observer mode
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: '张三',
age: 20
});
function print() {
console.log(`${person.name}, ${person.age}`)
}
observe(print);
person.name = '李四';
// 输出
// 李四, 20
end
This article mainly refers to Yifeng es6 tutorial , js Red Book Fourth Edition
Due to my limited level, if there is any mistake, please contact me to point it out, thank you.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。