Learning notes of javascript advanced programming | 9.1 Agent basis

follow [front end Xiaoao] , read more original technical articles

Agent basis

  • ES6 proxy and reflection provide developers with the ability to intercept and embed additional behavior into basic operations
  • A proxy is an abstraction of a target object. It can be used as a substitute for the target object, but it is completely independent of the target object
  • The target object can be operated directly or through the agent. Direct operation will bypass the behavior given by the agent

Relevant code →

Create empty proxy

  • Use the Proxy constructor to create a Proxy and receive two parameters: the target object and the handler object
  • Empty proxy is the simplest proxy. Empty object can be used as handler object, and empty proxy object can only be used as an abstract target object
const target = {
  // Target object
  id: 'target',
}
const handler = {} // Handler object (empty object)
const proxy = new Proxy(target, handler) // Create empty proxy
  • (by default) all operations performed on an empty delegate object are applied to the target object and vice versa
console.log(target.id) // 'target'
console.log(proxy.id) // 'target'

target.id = 'foo' // Target object attribute reassignment
console.log(target.id) // 'foo'
console.log(proxy.id) // 'foo' will be reflected on the agent

proxy.id = 'bar' // Proxy attribute reassignment
console.log(target.id) // 'bar' will be reflected on the target object
console.log(proxy.id) // 'bar'

console.log(target.hasOwnProperty('id')) // true
console.log(proxy.hasOwnProperty('id')) // true
  • The Proxy constructor does not have the prototype attribute and cannot be detected using the instanceof operator
console.log(Proxy) // [Function: Proxy]
console.log(Proxy.prototype) // undefined
console.log(proxy instanceof Proxy) // TypeError: Function has non-object prototype 'undefined' in instanceof check
  • Use strict equality = = = to distinguish between agents and targets
console.log(target === proxy) // false

Define catcher

  • The main purpose of using agents is to define traps, that is, interceptors of basic operations
  • Each catcher corresponds to a basic operation, which can be called directly or indirectly on the agent. When calling the operation, the catcher function will be called first, and then the operation will be propagated to the target object
const target2 = {
  foo: 'bar',
}
const handler2 = {
  // Define the get() catcher function with the method name as the key
  get() {
    return 'handler override'
  },
}
const proxy2 = new Proxy(target2, handler2)
  • The get() function can be triggered in various forms and intercepted by the Catcher: proxy[property], proxy Property and object create(proxy)[property]
  • The catcher is triggered only when an operation is performed on the proxy object, not on the target object
console.log(proxy2.foo) // 'handler override', operation on proxy object
console.log(proxy2['foo']) // 'handler override', operation on proxy object
console.log(Object.create(proxy2).foo) // 'handler override', operation on proxy object

console.log(target2.foo) // 'bar', operation on the target object
console.log(target2['foo']) // 'bar', operation on the target object
console.log(Object.create(target2).foo) // 'bar', operation on the target object

Catcher parameters and reflection API s

The get() catcher receives three parameters: the target object, the property to query, and the proxy object

const target3 = {
  foo: 'bar',
}
const handler3 = {
  // The get() catcher receives three parameters: the target object, the property to query, and the proxy object
  get(tar, pro, rec) {
    console.log(tar === target3)
    console.log(pro)
    console.log(rec === handler3)
  },
}
const proxy3 = new Proxy(target3, handler3)
proxy3.foo
/* 
  true
  'foo'
  false
*/
  • The catcher uses these parameters to reconstruct the original behavior of the captured method
const handler4 = {
  get(tar, pro, rec) {
    return tar[pro] // target3['foo']
  },
}
const proxy4 = new Proxy(target3, handler4)
console.log(proxy4.foo) // 'bar'
  • All catcher methods that process objects have corresponding reflection API methods (with the same name and the same behavior), which exist on the global object Reflect
const handler5 = {
  get() {
    return Reflect.get(...arguments) // Decoupling with arguments
  },
  // get: Reflect.get, / / more concise
}
const proxy5 = new Proxy(target3, handler5)
console.log(proxy5.foo) // 'bar'
  • Create an empty proxy that can capture all methods and forward each method to the reflection API without defining a handler object
const proxy6 = new Proxy(target3, Reflect)
console.log(proxy6.foo) // 'bar'
  • Using the reflection API, the captured method can be modified with minimal code
const target4 = {
  foo: 'bar',
  baz: 'qux',
}
const handler6 = {
  get(tar, pro, rec) {
    let dec = ''
    pro === 'foo' && (dec = '!!!')
    return Reflect.get(...arguments) + dec
  },
}
const proxy7 = new Proxy(target4, handler6)
console.log(proxy7.foo) // 'bar!!!'
console.log(proxy7.baz) // 'qux'

Capture invariant

  • The behavior of the capture handler must follow the catcher invariant
  • For example, if the target object has a non configurable and non rewritable property, the catcher will report an error when modifying the return value
const target5 = {}
Object.defineProperty(target5, 'foo', {
  configurable: false, // Not configurable
  writable: false, // Non rewritable
  value: 'bar',
})
const handler7 = {
  get() {
    return 'qux'
  },
}
const proxy8 = new Proxy(target5, handler7)
console.log(proxy8.foo) // TypeError: 'get' on proxy: property 'foo' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'bar' but got 'qux')

Revocable agent

  • The proxy created by new Proxy() is irrevocable and will exist throughout the life cycle of the proxy object
  • Proxy. The revocable () method can be used to create a revocable proxy object
    • Receiving target object and processing object
    • Returns an object with a structure of {"proxy":proxyObj,"revoke":revokeFun}
    • Proxy is the proxy object; revoke is a revocation method. No parameters are required when calling
    • Undo the function revoke() idempotent, and call it multiple times with the same result
    • The revocation operation is irreversible. Calling the agent again after revocation will report an error
const target6 = {
  foo: 'bar',
}
const handler8 = {
  get() {
    return 'intercepted'
  },
}
const revocable = Proxy.revocable(target6, handler8)
const proxy9 = revocable.proxy // Create revocable proxy
console.log(proxy9.foo) // 'intercepted'

revocable.revoke() // Revocation of agency
revocable.revoke() // Revoke the proxy and call it multiple times with the same result
revocable.revoke() // Revoke the proxy and call it multiple times with the same result
// console.log(proxy9.foo) // TypeError: Cannot perform 'get' on a proxy that has been revoked

Practical reflection API

Reflection API and object API

  • The reflection API is not limited to capture handlers
  • Most reflection API s have corresponding methods on Object types:
    • The method on Object is suitable for general programs, and the reflection method is suitable for fine-grained Object control and operation

Status flag

  • The following reflection method provides a status flag and returns a Boolean value indicating whether the operation was successful
    • Reflect.defineProperty(),Reflect.preventExtensions(),Reflect.setPrototypeOf(),Reflect.set(),Reflect.deleteProperty()
    • (the parameter format is correct) when the operation fails, an error will not be thrown, but false will be returned
const o = {}
Object.defineProperty(o, 'foo', {
  writable: false, // Non rewritable
})

Object.defineProperty(o, 'foo', { value: 'bar' }) // TypeError: Cannot redefine property: foo,Object. If defineproperty() is not defined successfully, an error will be thrown
Reflect.defineProperty(o, 'foo', { value: 'bar' }) // Reflect.defineProperty() is not successfully defined and will not throw an error
console.log(Reflect.defineProperty(o, 'foo', { value: 'bar' })) // false,Reflect.defineProperty() returns the Boolean value of the status tag

// Refactored code
if (Reflect.defineProperty(o, 'foo', { value: 'bar' })) {
  console.log('success')
} else {
  console.log('failure') // 'failure'
}

Replace operators with first-class functions

  • The following reflection methods provide operations that can only be done by operators
    • Reflect.get(): can replace the object property access operator
    • Reflect.set(): can replace the assignment operator=
    • Reflect.has(): can replace the in operator or with()
    • Reflect.deleteProperty(): can replace the delete operator
    • Reflect.construct(): can replace the new operator
const o2 = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar
  },
}
Reflect.get(o2, 'foo') // 1
Reflect.set(o2, 'foo', 3)
console.log(o2.foo) // 3
Reflect.has(o2, 'foo') // true
Reflect.deleteProperty(o2, 'bar')
console.log(o2.bar) // undefined
const arr = Reflect.construct(Array, [1, 2, 3])
console.log(arr) // [ 1, 2, 3 ]

Safely apply functions

  • For function prototype object function When the apply method of prototype uses call to bind, reflect Apply () makes the code simpler and easier to understand
const f1 = function () {
  console.log(arguments[0] + this.mark)
}
const o3 = {
  mark: 95,
}
f1.apply(o3, [15]) // 110, bind this of f1 to o3
Function.prototype.apply.call(f1, o3, [15]) // 110. The apply method of the prototype object of the function is bound by call
Reflect.apply(f1, o3, [15]) // 110. Initiate the call to the objective function through the specified parameter list, including three parameters (objective function, bound this object and argument list)

Detailed documentation on the Reflect object →

Acting for another agent

  • Create an agent and use it to proxy another agent, so as to build a multi-layer interception network on a target object
const target7 = {
  foo: 'bar',
}
const firstProxy = new Proxy(target7, {
  // Tier 1 agent
  get() {
    console.log('first proxy')
    return Reflect.get(...arguments)
  },
})
const secondProxy = new Proxy(firstProxy, {
  // Second tier agent
  get() {
    console.log('second proxy')
    return Reflect.get(...arguments)
  },
})
console.log(secondProxy.foo)
/* 
  'second proxy'
  'first proxy'
  'bar'
*/

Problems and deficiencies of agency

this in proxy

  • The value of this in the proxy is a potential source of problems. For example, this in a method usually points to the object calling the method
const target8 = {
  thisValEqualProxy() {
    return this === proxy10
    /* 
      this Point to:
      In an instance, point to the instance itself
      In a proxy, point to the proxy object
    */
  },
}
const proxy10 = new Proxy(target8, {})
console.log(target8.thisValEqualProxy()) // false
console.log(proxy10.thisValEqualProxy()) // true
  • When the target object depends on the object identity, the pointing of this will cause problems
const wm = new WeakMap()
class User {
  constructor(userId) {
    wm.set(this, userId) // Use the target object as the key for the WeakMap
    /* 
      this Point to: target object
    */
  }
  get id() {
    return wm.get(this)
    /* 
      this Direction of:
      In the instance, point to the instance itself User {}
      In a proxy, point to the proxy object
    */
  }
}

const user = new User(123)
console.log(wm) // WeakMap {User => 123}
console.log(user.id) // 123

const userInstanceProxy = new Proxy(user, {}) // Proxy user instance. this in the user class constructor points to the user class instance
console.log(wm) // Weakmap {user = > 123}, weak key unchanged
console.log(userInstanceProxy.id) // undefined
  • Change the proxy instance to the proxy class itself, and then create the proxy instance to solve the problem
const userClassProxy = new Proxy(User, {}) // Proxy User class itself
const proxyUser = new userClassProxy(456) // Create a proxy instance. this in the User class constructor points to the proxy instance
console.log(wm) // Weakmap {user = > 123, user = > 456}, the weak key changes, and the proxy key is added
console.log(proxyUser.id) // 456

Agent and internal slot

  • Some built-in types may rely on mechanisms beyond the control of the proxy: for example, the execution of a method of type Date depends on the internal slot [[NumberDate]] on this value, which does not exist in the proxy object and cannot be accessed by get() or set() operations
const target9 = new Date()
const proxy11 = new Proxy(target9, {})
console.log(target9.getDate()) // 24. Date of the day
console.log(proxy11.getDate()) // TypeError: this is not a Date object.

Summary & ask questions

  • What is the use of an agent? How does it relate to the target object?
  • How to create an empty proxy? How to distinguish between empty proxy objects and target objects?
  • What is a trap? How is it invoked and triggered? How can the get() function be intercepted by the catcher?
  • What parameters does the get() catcher receive? Write a piece of code that uses these parameters to rewrite the original behavior of the capture method
  • How do I create a revocable proxy? What happens if you undo it again? What happens when a proxy is revoked?
  • How to understand the reflection object? What are the similarities and differences between its reflection API and object API? How to understand Reflect Apply() method?
  • How to build a multi-layer interception network on a target object through an agent?
  • What are the potential problems of the agent? How to solve it?

Keywords: Javascript Front-end Proxy

Added by jibosh on Sun, 30 Jan 2022 18:19:17 +0200