! [image_1e4411le1meo5qg1v8nllo7ov1g. PNG 1150.8 kB] [1]

Only hear empty photograph loudly way: “please road long report zhang true person immediately, the matter is urgent, a moment delay can’t!” The man said, “Unluckily, the master has come here. My master has been in prison for more than a year, and my disciples have not seen him for a long time.”

preface

In wuxia novels, we often see such scenes. A certain wu Lin personage comes to visit the respected gang leader, often need to pass under his disciple’s notification. If the master was out or did not want to see the person, he would ask the disciple to decline politely.

The Proxy we’re going to talk about today is kind of like this. As the name suggests, a Proxy is a Proxy for other objects to control access to this object.

[Proxy][2], [Reflect][3], [Function][4], [extension operator][5] and other knowledge will be involved in this paper. It is mainly practical, and the grammar will not be explained in detail. It is recommended to use the relevant chapters in [ES6 Introduction][6] by Ruan Yfeng.

! [code. PNG 74.3 kB] [7]

1. What interception methods does Proxy provide?

Proxy is generally used to set up a layer of interception on the target object, to achieve the control of the access and modification of the target object. Proxy is a constructor that needs to be used in conjunction with the new operator.

! [image. PNG 21.9 kB] [8]

The Proxy constructor takes two arguments. The first argument is the object to intercept. This object can only be an object, array, or function.

The second parameter is a configuration object that provides the interception method.

Const person = {name: 'Tom'} const proxy = new proxy (person, {}); proxy === person; Const proxy = new proxy (person, {get(target, prop) {console.log(' ${prop} is ${target[prop]} '); return target[prop]; } }) proxy.name // 'name is tom'Copy the code

Proxy supports 13 interception operations, four of which will be highlighted in this article.

! [image_1e3br0h5t2kvb5q5mo1r8t79c9. PNG 127.3 kB] [9]

  1. Get (target, Prop, receiver) : intercepts access to object properties.
  2. Set (target, Prop, value, receiver) : intercepts the setting of an object property and returns a Boolean value.
  3. apply(target, object, args): used to intercept calls to functions such asproxy().
  4. construct(target, args)The: method intercepts the new operator, such asnew proxy(). In order for the new operator to take effect on the generated Proxy object, the target object used to initialize the Proxy itself must have the [[Construct]] internal method (that is, the new Target must be valid).
  5. Has (target, prop) : Intercepts operations such as Prop in proxy and returns a Boolean value.
  6. deleteProperty(target, prop): Intercept for exampledelete proxy[prop]Returns a Boolean value.
  7. ownKeys(target)Intercept:Object.getOwnPropertyNames(proxy),Object.keys(proxy),for inYou loop and so on, and you end up returning an array.
  8. getOwnPropertyDescriptor(target, prop)Intercept:Object.getOwnPropertyDescriptor(proxy, propKey)Returns the description object for the property.
  9. defineProperty(target, propKey, propDesc)Intercept:Object.defineProperty(proxy, propKey, propDesc),Object.defineProperties(proxy, propDescs)Returns a Boolean value.
  10. preventExtensions(target)Intercept:Object.preventExtensions(proxy)Returns a Boolean value.
  11. getPrototypeOf(target)Intercept:Object.getPrototypeOf(proxy)Returns an object.
  12. isExtensible(target)Intercept:Object.isExtensible(proxy)Returns a Boolean value.
  13. setPrototypeOf(target, proto)Intercept:Object.setPrototypeOf(proxy, proto)Returns a Boolean value. If the target object is a function, there are two additional operations that can be intercepted.

2. Proxy vs Object.defineProperty

Before Proxy, JavaScript provided object.defineProperty to allow getter/setter interception of an Object, so what’s the difference?

! [image_1e442ld7g6legdo15362m1ke733. PNG 109.3 kB] [10]

2.1 Object.definePropertyCannot listen for all properties

Object.defineproperty cannot listen for all the properties of an Object at once. It must be implemented iteratively or recursively.

   let girl = {
     name: "marry".age: 22
   }
   /* Proxy listens to the entire object */
   girl = new Proxy(girl, {
     get() {}
     set(){}})/* Object.defineProperty */
   Object.keys(girl).forEach(key= > {
     Object.defineProperty(girl, key, {
       set() {},
       get(){}})})Copy the code

2.2 Object.definePropertyUnable to listen for newly added properties

Proxy can listen to new properties, Object. DefineProperty cannot, so you need to manually listen again. Therefore, to dynamically listen on a property in Vue, it is usually added in the form vue.set (girl, “hobby”, “game”).

   let girl = {
     name: "marry".age: 22
   }
   /* Proxy listens to the entire object */
   girl = new Proxy(girl, {
     get() {}
     set(){}})/* Object.defineProperty */
   Object.keys(girl).forEach(key= > {
     Object.defineProperty(girl, key, {
       set() {},
       get(){}})});/* Proxy is valid, object.defineProperty is not valid */
   girl.hobby = "game"; 
Copy the code

2.3. Object.definePropertyUnable to respond to array operation

Object. DefineProperty can listen for array changes, but Object. DefineProperty cannot respond to push, shift, pop, unshift and other methods.

   const arr = [1.2.3];
   /* Proxy listens to arrays */
   arr = new Proxy(arr, {
     get() {},
     set(){}})/* Object.defineProperty */
   arr.forEach((item, index) = > {
     Object.defineProperty(arr, `${index}`, {
       set() {},
       get() {}
     })
   })
   
   arr[0] = 10; / / to take effect
   arr[3] = 10; // Only Proxy is valid
   arr.push(10); // Only Proxy is valid
Copy the code

Object. DefineProperty is still not heard for the newly added array entry. Therefore, in order to listen for array changes in Mobx, the default array length is set to 1000, listening for attribute changes from 0 to 999.

   /* Mobx implementation */
   const arr = [1.2.3];
   /* Object.defineProperty */
   [...Array(1000)].forEach((item, index) = > {
     Object.defineProperty(arr, `${index}`, {
       set() {},
       get(){}})}); arr[3] = 10; / / to take effect
   arr[4] = 10; / / to take effect
Copy the code

What do I do if I want to listen to push, shift, pop, unshift, etc? In both Vue and Mobx, this is done by rewriting the prototype.

When defining a variable, determine if it is an array. If it is an array, change its __proto__ to point to subArrProto, thereby overwriting the prototype chain.

   const arrayProto = Array.prototype;
   const subArrProto = Object.create(arrayProto);
   const methods = ['pop'.'shift'.'unshift'.'sort'.'reverse'.'splice'.'push'];
   methods.forEach(method= > {
     /* Override the prototype method */
     subArrProto[method] = function() {
       arrayProto[method].call(this. arguments); };/* Listen to these methods */
     Object.defineProperty(subArrProto, method, {
       set() {},
       get(){}})})Copy the code

2.4 More Proxy interception modes

Proxy provides 13 intercepting methods, including Constructor, apply, deleteProperty, etc., while object.defineProperty only has get and set.

2.5. Object.definePropertyBetter compatibility

Proxy is a new API, compatibility is not good enough, do not support IE full series.

3. Grammar

3.1 the get

The get method is used to intercept readings of the properties of the target object. It takes three parameters: the target object, the property name, and the Proxy instance itself. Based on the properties of the get method, you can implement a lot of practical functions, such as setting the private property in the object (generally defined properties we start with _ to indicate the private property), implement the function of blocking access to the private property.

const person = {
    name: 'tom',
    age: 20,
    _sex: 'male'
}
const proxy = new Proxy(person, {
    get(target, prop) {
        if (prop[0] === '_') {
            throw new Error(`${prop} is private attribute`);
        }
        return target[prop]
    }
})
proxy.name; // 'tom'
proxy._sex; // _sex is private attribute
Copy the code

You can also set default values for undefined properties in an object. By intercepting access to the property, if undefined, it returns the default value set originally.

let person = {
    name: 'tom',
    age: 20
}
const defaults = (obj, initial) => {
    return new Proxy(obj, {
        get(target, prop) {
            if (prop in target) {
                return target[prop]
            }
            return initial
        }
    })
}
person = defaults(person, 0);
person.name // 'tom'
person.sex // 0
person = defaults(person, null);
person.sex // null
Copy the code

3.2 the set

The set method intercepts an assignment to a property. Typically, it takes four parameters: the target object, the property name, the property value, and the Proxy instance. Here is a use of the set method that prints out the current state of an attribute when it is assigned.

const proxy = new Proxy({}, {
    set(target, key, value, receiver) {
        console.log(`${key} has been set to ${value}`);
        Reflect.set(target, key, value);
    }
})
proxy.name = 'tom'; // name has been setted ygy
Copy the code

The fourth parameter, receiver, refers to the current Proxy instance, which in this example refers to Proxy.

const proxy = new Proxy({}, {
    set(target, key, value, receiver) {
        if (key === 'self') {
            Reflect.set(target, key, receiver);
        } else {
            Reflect.set(target, key, value);
        }
    }
})
proxy.self =
proxy.self === proxy; // true
Copy the code

If you’ve ever written a form validation, you might be confused by all the validation rules. Proxy can intercept the fields in the form for format verification when filling in the form. In general, people use an object to hold validation rules, which makes it easier to extend the rules.

// Validation rules
const validators = {
    name: {
        validate(value) {
            return value.length > 6;
        },
        message: 'User name length must not be less than six'
    },
    password: {
        validate(value) {
            return value.length > 10;
        },
        message: 'Password length must not be less than 10'
    },
    moblie: {
        validate(value) {
            return / ^ 1 (3, 5 7 8 | | | | 9) [0-9] {9} $/.test(value);
        },
        message: 'Incorrect format of phone number'}}Copy the code

Then write the validation method, use the set method to set the properties of the form object to intercept, and use the above validation rules to verify the property value when intercepting, if the verification fails, the pop-up prompt.

Function validator(obj, validators) {return new Proxy(obj, target, key) value) { const validator = validators[key] if (! validator) { target[key] = value; } else if (validator.validate(value)) { target[key] = value; } else { alert(validator.message || ""); } } }) } let form = {}; form = validator(form, validators); form.name = '666'; Form. password = '113123123123123';Copy the code

However, if the property has been set to unwritable, the set will not take effect (but the set method will still execute).

const person = {
    name: 'tom'
}
Object.defineProperty(person, 'name', {
    writable: false
})
const proxy = new Proxy(person, {  
    set(target, key, value) {
        console.log(666)
        target[key] = 'jerry'
    }
})
proxy.name = '';
Copy the code

3.3. The apply

Apply is typically used to intercept a call to a function. It takes three arguments, namely the target object, the context object (this), and an array of arguments.

function test() {
    console.log('this is a test function');
}
const func = new Proxy(test, {
    apply(target, context, args) {
        console.log('hello, world');
        target.apply(context, args);
    }
})
func();
Copy the code

Using the apply method, you can obtain the number of times a function has been executed. You can also print the elapsed time of the function execution, which is often used for performance analysis.

function log() {}
const func = new Proxy(log, {
    _count: 0,
    apply(target, context, args) {
        target.apply(context, args);
        console.log(`this function has been called ${++this._count} times`);
    }
})
func()
Copy the code

3.4. The construct

The construct method is used to intercept the new operator. It takes three arguments, the target object, the constructor argument list, the Proxy object, and finally needs to return an object. Here’s an example of how to use it:

function Person(name, age) { this.name = name; this.age = age; } const P = new Proxy(Person, { construct(target, args, newTarget) { console.log('construct'); return new target(... args); } }) const p = new P('tom', 21); // 'construct'Copy the code

We know that if the constructor returns nothing or a value of the original type, then the default value is this, and if it returns a value of the reference type, then the final value of new is this. Therefore, you can proxy an empty function and return a new object.

function noop() {}
const Person = new Proxy(noop, {
    construct(target, args, newTarget) {
        return {
            name: args[0],
            age: args[1]
        }
    }
})
const person = new Person('tom', 21); // { name: 'tom', age: 21 }
Copy the code

4. What interesting things can Proxy do?

Proxy can be used in a wide range of scenarios. It can be used to intercept the set/get of an object to achieve data response. In both Vue3 and Mobx5, Proxy is used instead of object.defineProperty. So let’s take a look at what a Proxy can do.

4.1 SAO operation: proxy class

Construct can be used to Proxy classes. You might be wondering if a Proxy can only Proxy Object types. How should classes be represented?

! [image_1e440c3701ds37061o8goo316pk13.png-36kB][11]

In fact, the essence of a class is also a constructor and prototype (object) composition, it can be completely surrogate. Consider the need to intercept access to properties and calculate the execution time of functions on the prototype, and it becomes clear what to do. You can set GET interception for properties and apply interception for prototype functions.

Consider intercepting the following prototype function of the Person class. Use Object. GetOwnPropertyNames all the above function to get the prototype, traverse the function and the apply of the interceptor.

class Person { constructor(name, age) { this.name = name; this.age = age; } say() { console.log(`my name is ${this.name}, and my age is ${this.age}`) } } const prototype = Person.prototype; / / get all the attributes of the Object on the prototype getOwnPropertyNames (prototype). ForEach ((name) = > {Person. The prototype [name] = new Proxy(prototype[name], { apply(target, context, args) { console.time(); target.apply(context, args); console.timeEnd(); }})})Copy the code

After intercepting the prototype function, you start thinking about intercepting access to properties. The construct method can be used to block access to all properties when new is created. Yes, since the instance of new is also an object, it is perfectly possible to intercept this object.

New Proxy(Person, {// construct(target, args) {const obj = new target(... args); Return new Proxy(obj, {get(target, prop) {console.log(' ${target.name}.${prop} is being getting '); return new Proxy(obj, {get(target, prop) {console.log(' ${target.name}. return target[prop] } }) } })Copy the code

So, the final complete code looks like this:

class Person { constructor(name, age) { this.name = name; this.age = age; } say() { console.log(`my name is ${this.name}, and my age is ${this.age}`) } } const proxyTrack = (targetClass) => { const prototype = targetClass.prototype; Object.getOwnPropertyNames(prototype).forEach((name) => { targetClass.prototype[name] = new Proxy(prototype[name], { apply(target, context, args) { console.time(); target.apply(context, args); console.timeEnd(); } }) }) return new Proxy(targetClass, { construct(target, args) { const obj = new target(... args); return new Proxy(obj, { get(target, prop) { console.log(`${target.name}.${prop} is being getting`); return target[prop] } }) } }) } const MyClass = proxyTrack(Person); const myClass = new MyClass('tom', 21); myClass.say(); myClass.name;Copy the code

4.2 Can’t Wait optional Chain: Deep Value (get)

Usually when fetching data, often encounter deep data structure, if do not do any processing, it is easy to cause JS error. To avoid this problem, you might use more than one && :

const country = {
    name: 'china',
    province: {
        name: 'guangdong',
        city: {
            name: 'shenzhen'
        }
    }
}
const cityName = country.province
    && country.province.city
    && country.province.city.name;
Copy the code

But this is still too tedious, so Lodash provides the get method to help with this:

_.get(country, 'province.city.name');
Copy the code

It looks good, but there’s something wrong (good, but ugly). The latest ES proposal provides syntactic sugar for optional chains, allowing us to use the following syntax for deep values.

country? .province? .city? .nameCopy the code

However, this feature is only at stage3 and has not yet been formally incorporated into the ES specification, let alone supported by browsers. So, we had to find another way. At this point you might wonder if you could use the Proxy’s get method to intercept access to a property. Would that allow you to implement deep values?

! [code. PNG 92.3 kB] [12]

Next, I’ll walk you through the implementation of the get method below.

Const obj = {person: {}} // Expected result get(obj)() === obj; get(obj).person(); // {} get(obj).person.name(); // undefined get(obj).person.name.xxx.yyy.zzz(); // undefinedCopy the code

First, create a GET method that intercepts incoming objects using get in the Proxy.

function get (obj) { return new Proxy(obj, { get(target, prop) { return target[prop]; }})}Copy the code

Let’s run the three examples above and see what happens:

get(obj).person; // {}
get(obj).person.name; // undefined
get(obj).person.name.xxx.yyy.zzz; // Cannot read property 'xxx' of undefined
Copy the code

The first two test cases were successful, but the third failed because get(obj).person.name is undefined, so the focus is on handling the case where the attribute is undefined. This time, instead of returning the target[prop] directly, the get method returns a proxy object, so that the third example does not report an error.

function get (obj) { return new Proxy(obj, { get(target, prop) { return get(target[prop]); }})}Copy the code

If target[prop] is undefined, then the first parameter to the Proxy must be an object. Therefore, special treatment is required when obj is undefined. In order to be able to deeply value, only the default value of undefined must be set to an empty object.

function get (obj = {}) { return new Proxy(obj, { get(target, prop) { return get(target[prop]); } }) } get(obj).person; // {} get(obj).person.name; // {} get(obj).person.name.xxx.yyy.zzz; / / {}Copy the code

The last two return values are not correct, although no error is reported. If you do not set the default value to empty, the object cannot be accessed, and setting the default value to empty changes the return value. What was to be done? Take a closer look at the expected design above and see if there is a missing parenthesis, which is why each property is executed as a function. So you need to modify this function slightly to support the apply interception approach.

Function noop() {} function get (obj) {return new Proxy(noop, {// context, [arg]) { return obj; }, get(target, prop) { return get(obj[prop]); }})}Copy the code

So this get method can already be used like this.

get(obj)() === obj; // true
get(obj).person.name(); // undefined
get(obj).person.name.xxx.yyy.zzz(); // Cannot read property 'xxx' of undefined
Copy the code

Our ideal would be to return undefined if the attribute is undefined, but still support access to the parent attribute, rather than throwing an error. If you follow this line of thinking, it’s obvious that when the property is undefined, you need to use the Proxy to do something special. So we need a get method with the following properties:

get(undefined)() === undefined; // true
get(undefined).xxx.yyy.zzz() // undefined
Copy the code

Unlike previous problems, there is no need to pay attention to whether get(undefined).xxx is the correct value, because to get the value you have to execute to get it. So all you need to do is default to undefined for all properties that are accessed after undefined.

function noop() {} function get (obj) { if (obj === undefined) { return proxyVoid; } return new Proxy(noop, {//}) {return new Proxy(noop, {//}) [arg]) { return obj === undefined ? arg : obj; }, get(target, prop) { if ( obj ! == undefined && obj ! == null && obj.hasOwnProperty(prop) ) { return get(obj[prop]); } return proxyVoid; }})}Copy the code

Now let’s think about how this proxyVoid function can be implemented. Obviously it should be an object that returns with undefined proxied. Why don’t we just do this?

const proxyVoid = get(undefined);
Copy the code

But this obviously creates an infinite loop, so you need to determine the threshold so that the first time get receives undefined, it doesn’t loop.

let isFirst = true; function noop() {} let proxyVoid = get(undefined); function get(obj) { if (obj === undefined && ! isFirst) { return proxyVoid; } if (obj === undefined && isFirst) { isFirst = false; } return new Proxy(noop, {//}) {return new Proxy(noop, {//}) [arg]) { return obj === undefined ? arg : obj; }, get(target, prop) { if ( obj ! == undefined && obj ! == null && obj.hasOwnProperty(prop) ) { return get(obj[prop]); } return proxyVoid; }})}Copy the code

Let’s check again whether this method is feasible:

get(obj)() === obj; // true
get(obj).person.name(); // undefined
get(obj).person.name.xxx.yyy.zzz(); // undefined
Copy the code

And Bingo, this is exactly what we need. Finally, the complete code looks like this:

let isFirst = true; function noop() {} let proxyVoid = get(undefined); function get(obj) { if (obj === undefined) { if (! isFirst) { return proxyVoid; } isFirst = false; } return new Proxy(noop, {//}) {return new Proxy(noop, {//}) [arg]) { return obj === undefined ? arg : obj; }, get(target, prop) { if ( obj ! == undefined && obj ! == null && obj.hasOwnProperty(prop) ) { return get(obj[prop]); } return proxyVoid; } }) } this.get = get;Copy the code

This proxy-based get method was inspired by a Github library called Safe-touch, which is implemented in the source code: [Safe-touch][13]

4.3 the pipe

In the latest ECMA proposal, a native of pipeline operator | >, in RxJS and NodeJS have similar concept of pipe.

! [image_1e445iuci168nj1m1stvkbm1jhg4a. PNG 13.7 kB] [14]

The pipe function can also be implemented using a Proxy, which can be easily implemented by intercepting access to properties using GET, putting the accessed methods in a stack array, and returning the result once execute is accessed.

const pipe = (value) => {
    const stack = [];
    const proxy = new Proxy({}, {
        get(target, prop) {
            if (prop === 'execute') {
                return stack.reduce(function (val, fn) {
                    return fn(val);
                }, value);
            }
            stack.push(window[prop]);
            return proxy;
        }
    })
    return proxy;
}
var double = n => n * 2;
var pow = n => n * n;
pipe(3).double.pow.execute;
Copy the code

Note: in order to store the method in the stack, we use the form window[prop] to get the corresponding method. You can also mount the double and POw methods into an object that replaces the window.