JS advanced programming

Proxy and reflection

1, Agent basis

ECMAScript's new proxy and reflection provides developers with the ability to intercept and embed additional behavior in a basic operation.
A proxy is an abstraction of the target object.

1. Create an empty agent

An empty proxy does nothing but act as an abstract target object.

The Proxy is created using the Proxy constructor. This constructor takes two parameters: the target object and the handler object. TypeError will be thrown if any parameter is missing.
The code is as follows (example):

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 a value to the target property is reflected on both objects
//Two objects access the same value
target.id='bar';
console.log(target.id);//bar
console.log(proxy.id);//bar
//Assigning a value to a proxy property will reflect on both objects
//The assignment is transferred to the target object
proxy.id='foo';
console.log(target.id);//foo
console.log(proxy.id);//foo

2. Define the catcher

The main purpose of defining an agent is to use the catcher. The catcher is the "interceptor of basic operations" defined in the handler object.
Only when these operations (proxy[property], proxy.property, Object.create(proxy)[property]) are performed on the proxy object will the catcher be triggered. Performing these operations on the target object still produces normal behavior.
The code is as follows (example):

//Define get() catcher
const target={
  foo:'bar'
};
const handler={
  //The catcher takes the method name as the key in the handler object
  get(){
    return 'handler override';
  }
}
const proxy=new Proxy(target,handler);
console.log(target.foo);//bar
console.log(proxy.foo);//handler override
console.log(target['foo']);//bar
console.log(proxy['foo']);//handler override
console.log(Object.create(target)['foo']);//bar
console.log(Object.create(proxy)['foo']);//handler override

3. Trap parameters and reflection API

All captors can access the corresponding parameters, based on which the original behavior of the captured method can be reconstructed.
The code is as follows (example):

const target={
  foo:'bar'
};
const handler={
  get(trapTarget,property,receiver){
    console.log(trapTarget==target);
    console.log(property);
    console.log(receiver==proxy);
  }
}
const proxy=new Proxy(target,handler);
proxy.foo;
//true
//foo
//true

All methods that can be captured in the handler object have corresponding reflection API methods. These methods have the same name and function signature as the method intercepted by the catcher, and also have the same behavior as the intercepted method.
The code is as follows (example):

const target={
  foo:'bar'
};
const handler={
  get(){
    return Reflict.get(...arguments);
  }
};
const proxy=new Proxy(target,handler);
console.log(proxy.foo);//bar
console.log(target.foo);//bar

4. Catcher invariants

According to ECMAScript specification, each captured method knows the target object context and capture function signature, and the behavior of the capture handler must follow the "catcher invariant".
The code is as follows (example):

const target={};
Object.defineProperty(target,'foo'{
  consigurable:false,
  writable:false,
  value:'bar',
};
const handler={
  get(){
    return 'qux';
  }
}
const proxy=new Proxy(target,handler);
console.log(proxy.foo);
//TypeError

5. Revocable agency

The revocable() method supports the disassociation of the proxy object from the target object. Revoking an agent is irreversible. Moreover, the Undo function (revoke()) is idempotent, and the result is the same how many times it is called. Calling the agent after revoking the agent will throw a TypeError.
The code is as follows (example):

const target={
  foo:'bar'
};
const handler={
  get(){
    return 'itercepted';
  }
};
const {proxy,revoke} = Proxy.revokable(target,handler);
console.log(proxy.foo);//itercepted
console.log(target.foo);//bar
revoke();
console.log(proxy.foo);//TypeError

6. Practical reflection API

Reflection API and object API
When using reflection API, pay attention to:

  1. The reflection API is not limited to capture handlers
  2. Most reflection API methods have corresponding methods on Object types

Status flag
Many reflection methods return Boolean values called "status flags" to indicate whether the intended operation was successful.
The following reflection methods provide status markers:

  • Reflect.defineProperty()
  • Reflect.preventExtensions()
  • Reflect.setPrototypeOf()
  • Reflect.set()
  • Reflect.deleteProperty()

Replacing operators with first-order functions
The following reflection methods provide operations that can only be done by operators:

  • Reflect.get(): you can override the object property access operator
  • Reflect.set(): can replace = assignment operator
  • Reflect.has(): can replace the in operator or with()
  • Reflect.deleteProperty(): can replace the delete operator
  • Reflect.constructor(): can replace the new operator

Safely apply functions

7. Acting for another agent

The code is as follows (example):

const target={
  foo:'bar'
};
const firstProxy=new Proxy(target,{
  get(){
    console.log('first proxy');
    return Reflect.get(...arguments);
  }
});
const secondProxy=new Proxy(target,{
  get(){
    console.log('second proxy');
    return Reflect.get(...arguments);
  }
});
console.log(secondProxy.foo);
//second proxy
//first proxy
//bar

2, Proxy catcher and reflection method

1.get()

The get () catcher is called in 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()

1. Return value
Unlimited return value

2. Operation of interception
 proxy.property
 proxy[property]
 Object.create(proxy)[property]
 Reflect.get(proxy,property,receiver)

3. Catcher handler parameters
 target: target object
 property: the string key property on the referenced target object
 receiver: proxy object or object inheriting proxy object

4. Catcher invariants
If target.property is not writable and configurable, the value returned by the handler must match target.property.
If target.property is not configurable and the [[Get]] attribute is undefined, the return value of the handler must also be undefined.

2.set()

The get() catcher is called in 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()

1. Return value
If true, it means success; Returning false means failure. TypeError will be thrown in strict mode.

2. Operation of interception
 proxy.property=value
 proxy[property]=value
 Object.create(proxy)[property]=value
 Reflect.set(proxy,property,value,receiver)

3. Catcher handler parameters
 target: target object
 property: the string key property on the referenced target object
 value: the value to be assigned to the attribute
 receiver: the object receiving the initial assignment

4. Catcher invariants
If target.property is not writable and configurable, the value of the target property cannot be modified.
If target.property is not configurable and the [[Set]] attribute is undefined, the value of the target property cannot be modified.
In strict mode, TypeError will be thrown when the processing returns false.

3.has()

The has () catcher is called in the in operator. The corresponding reflection API method is Reflect.has().

const myTarget={};
const proxy=new Proxy(myTarget,{
  set(target,property){
    console.log('has()');
    return Reflect.has(...arguments);
  }
}
'foo' in proxy
//has()

1. Return value
has() must return a Boolean value indicating whether the property exists. Return non Boolean values are converted to Boolean values.

2. Operation of interception
 property in proxy
 property in Object.create(proxy)
 with(proxy) {(property);}
 Reflect.has(proxy,property)

3. Catcher handler parameters
 target: target object
 property: the string key property on the referenced target object

4. Catcher invariants
If target.property exists and is not configurable, the handler must return true.
If target.property exists and the target object is not extensible, the handler must return true.

4.defineProperty()

The defineProperty() catcher will be called in * * Object.defineProperty() *. The corresponding reflection API method is Reflect.defineProperty().

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()

1. Return value
defineProperty() must return a Boolean value indicating whether the property was successfully defined. Return non Boolean values are converted to Boolean values.

2. Operation of interception
 Object.defineProperty(proxy,property,descriptor)
 Reflect.defineProperty(proxy,property,descriptor)

3. Catcher handler parameters
 target: target object
 property: the string key property on the referenced target object
 descriptor: an object that contains optional enumerable, configurable, writable, value, get, and set definitions.

4. Catcher invariants
Properties cannot be defined if the target object is not extensible.
If the target object has a configurable property, you cannot add a non configurable property with the same name.
If the target object has a non configurable property, you cannot add a non configurable property with the same name.

5.getOwnPropertyDescriptor()

The getOwnPropertyDescriptor() catcher will be called in * * Object.getOwnPropertyDescriptor() *. The corresponding reflection API method is Reflect.getOwnPropertyDescriptor().

const myTarget={};
const proxy=new Proxy(myTarget,{
  getOwnPropertyDescriptor(target,property){
    console.log('getOwnPropertyDescriptor()');
    return Reflect.getOwnPropertyDescriptor(...arguments);
  }
}
Object.getOwnPropertyDescriptor(proxy,'foo');
//getOwnPropertyDescriptor()

1. Return value
getOwnPropertyDescriptor() must return an object or undefined if the property does not exist.

2. Operation of interception
 Object.getOwnPropertyDescriptor(proxy,property)
 Reflect.getOwnPropertyDescriptor(proxy,property)

3. Catcher handler parameters
 target: target object
 property: the string key property on the referenced target object

4. Catcher invariants
If its own target.property exists and is not configurable, the handler must return an object indicating the existence of the property.
If its own target.property exists and is configurable, the handler must return an object that is configurable for that property.
If its own target.property exists and the target is not extensible, the handler must return an object indicating the existence of the property.
If target.property does not exist and target is not extensible, the handler must return undefined to indicate that the property exists.
If target.property does not exist, the handler cannot return a configurable object with that property.

6.deleteProperty()

The deleteproperty () catcher is called in the delete operator. The corresponding reflection API method is Reflect.deleteProperty().

const myTarget={};
const proxy=new Proxy(myTarget,{
  deleteProperty(target,property){
    console.log('deleteProperty()');
    return Reflect.deleteProperty(...arguments);
  }
}
delete proxy.foo
//deleteProperty()

1. Return value
deleteProperty() must return a Boolean value indicating whether the property was deleted successfully. Return non Boolean values are converted to Boolean values.

2. Operation of interception
 delete proxy.property
 delete proxy[property]
 deleteProperty(proxy,property)

3. Catcher handler parameters
 target: target object
 property: the string key property on the referenced target object

4. Catcher invariants
If its own target.property exists and is not configurable, the handler cannot delete it.

7.ownKeys()

ownKeys() catcher will be called in * * Object.keys() * * and similar methods. The corresponding reflection API method is Reflect.ownKeys().

const myTarget={};
const proxy=new Proxy(myTarget,{
  ownKeys(target){
    console.log('ownKeys()');
    return Reflect.ownKeys(...arguments);
  }
}
Object.keys(proxy);
//ownKeys()

1. Return value
ownKeys() must return an enumerable object that contains a string or matches.

2. Operation of interception
 Object.getOwnPropertyNames(proxy)
 Object.getOwnPropertySymbols(proxy)
 Object.keysproxy)
 Object.ownkeys(proxy)

3. Catcher handler parameters
 target: target object

4. Catcher invariants
The returned enumerable object must contain all non configurable own properties of target.
If the target is not extensible, the returned enumerable object must accurately contain its own attribute key.

8.getPrototypeOf()

The getPrototypeOf() catcher will be called in * * Object.getPrototypeOf() *. The corresponding reflection API method is Reflect.getPrototypeOf().

const myTarget={};
const proxy=new Proxy(myTarget,{
  getPrototypeOf(target){
    console.log('getPrototypeOf()');
    return Reflect.getPrototypeOf(...arguments);
  }
}
Object.getPrototypeOf(proxy);
//getPrototypeOf()

1. Return value
getPrototypeOf() must return an object or null.

2. Operation of interception
 Object.getPrototypeOf(proxy)
 Reflect.getPrototypeOf(proxy)
 proxy.__proto__
 proxy instanceof Object

3. Catcher handler parameters
 target: target object

4. Catcher invariants
If target is not extensible, the only valid return value of Object.getPrototypeOf(proxy) is the return value of Object.getPrototypeOf(target).

9.setPrototypeOf()

The setPrototypeOf() catcher will be called in * * Object.setPrototypeOf() *. The corresponding reflection API method is Reflect.setPrototypeOf().

const myTarget={};
const proxy=new Proxy(myTarget,{
  setPrototypeOf(target,prototype){
    console.log('setPrototypeOf()');
    return Reflect.setPrototypeOf(...arguments);
  }
}
Object.setPrototypeOf(proxy);
//setPrototypeOf()

1. Return value
getPrototypeOf() must return a Boolean value indicating whether the prototype assignment is successful. Return non Boolean values are converted to Boolean values.

2. Operation of interception
 Object.setPrototypeOf(proxy)
 Reflect.setPrototypeOf(proxy)
 proxy.__proto__
 proxy instanceof Object

3. Catcher handler parameters
 target: target object
 property: alternative prototype of target, null if it is a top-level prototype

4. Catcher invariants
If target is not extensible, the only valid parameter is the return value of Object.getPrototypeOf(target).

10.isExtensible()

isExtensible() catcher will be called in * * Object.isExtensible() *. The corresponding reflection API method is Reflect.isExtensible().

const myTarget={};
const proxy=new Proxy(myTarget,{
  isExtensible(target){
    console.log('isExtensible()');
    return Reflect.isExtensible(...arguments);
  }
}
Object.isExtensible(proxy);
//isExtensible()

1. Return value
isExtensible() must return a Boolean value indicating whether the target is extensible. Return non Boolean values are converted to Boolean values.

2. Operation of interception
 Object.isExtensible(proxy)
 Reflect.isExtensible(proxy)

3. Catcher handler parameters
 target: target object

4. Catcher invariants
If the target is extensible, the handler must return true.
If the target is not extensible, the handler must return false.

11.preventExtensions()

The preventExtensions() catcher will be called in * * Object.preventExtensions() *. The corresponding reflection API method is Reflect.preventExtensions().

const myTarget={};
const proxy=new Proxy(myTarget,{
  preventExtensions(target){
    console.log('preventExtensions()');
    return Reflect.preventExtensions(...arguments);
  }
}
Object.preventExtensions(proxy);
//preventExtensions()

1. Return value
preventExtensions() must return a Boolean value indicating whether the target is no longer extensible. Return non Boolean values are converted to Boolean values.

2. Operation of interception
 Object.preventExtensions(proxy)
 Reflect.preventExtensions(proxy)

3. Catcher handler parameters
 target: target object

4. Catcher invariants
If Object.preventExtensions(proxy) is false, the handler must return true.

12.apply()

The apply () catcher is called when the function is called. The corresponding reflection API method is Reflect.apply().

const myTarget={};
const proxy=new Proxy(myTarget,{
  apply(target,thisArg,...argumentsList){
    console.log('apply()');
    return Reflect.apply(...arguments);
  }
}
(proxy);
//apply()

1. Return value
The return value is unlimited.

2. Operation of interception
 proxy(...argumentsList)
 Function.prototype.apply(thisArg,argumentsList)
 Function.prototype.call(thisArg,...argumentsList)
 Reflect.prototype.apply(thisArg,argumentsList)

3. Catcher handler parameters
 target: target object
 thisArg: this parameter when calling the function
 argumentsList: the list of parameters when calling a function

4. Catcher invariants
target must be a function object.

13.construct()

The construct () catcher is called in the new operator. The corresponding reflection API method is Reflect.construct().

const myTarget={};
const proxy=new Proxy(myTarget,{
  construct(target,argumentsList,newTarget){
    console.log('construct()');
    return Reflect.construct(...arguments);
  }
}
new proxy;
//construct()

1. Return value
construct() must return an object.

2. Operation of interception
 new proxy(...argumentsList)
 Reflect.construct(target,argumentsList,newTarget)

3. Catcher handler parameters
 target: target object
 argumentsList: the list of parameters when calling a function
 newTarget: the constructor initially called

4. Catcher invariants
target must be available as a constructor.

3, Agent mode

1. Tracking attribute access

By capturing operations such as get, set and has, you can know when object properties are accessed and queried. Put an object agent implementing the corresponding catcher in the application to monitor when and where the object has been accessed:

The code is as follows (example):

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

2. Hide attributes

3. Attribute verification

Because all assignment operations trigger the set() catcher, you can decide whether to allow or reject assignment according to the assigned value.

4. Function and constructor parameter verification

5. Data binding and observable objects

summary

Proxy is an exciting and dynamic new feature in ECMAScript6. Although it does not support backward compatibility, it opens up hitherto unknown new field of JavaScript meta programming and abstraction.
From a macro point of view, the proxy is the transparent abstraction layer of real JavaScript objects. Agents can define handler objects that contain traps that can intercept most of the basic operations and methods of JavaScript. In this catcher handler, you can modify the behavior of any basic operation, of course, following the catcher invariant.
The reflection API, which goes hand in hand with the agent, encapsulates a complete set of methods corresponding to the operations intercepted by the catcher. You can think of the reflection API as a set of basic operations, which are the basis of most JavaScript object APIs.
The application scenarios of agents are unlimited. Developers can use it to create various coding modes, such as tracking attribute access, hiding attributes, preventing modification or deletion of attributes, function parameter verification, constructor parameter verification, data binding, and observable objects.

Tags: Javascript

Posted on Wed, 27 Oct 2021 04:54:04 -0400 by dubrubru