11

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:

  1. Put some methods (such as Object.defineProperty) of the Object object that are obviously internal to the language on the Reflect object.
  2. 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.
  3. 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.
  4. 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.


greet_eason
482 声望1.4k 粉丝

技术性问题欢迎加我一起探讨:zhi794855679