[Translation] Use Proxy to monitor classes in Javascript

[Translation] Use Proxy to monitor classes in Javascript

Use Proxy to monitor classes in Javascript

Photo by Fabian Grohs on Unsplash

The Proxy object ( Proxy ) is a very cool but little-known feature of ES6. Although this feature has been around for a long time, I still want to explain it a bit in this article and illustrate its usage with an example.

What is Proxy

As the simple and boring definition on MDN:

The Proxy object is used to define custom behaviors for basic operations (such as attribute lookup, assignment, enumeration, function call, etc.).

Although this is a good summary, I haven't figured out what Proxy can do and what it can help us achieve.

First of all, the concept of Proxy comes from metaprogramming. Simply put, metaprogramming is the code that allows us to run the application (or core) code we write. For example, the notorious evalfunction allows us as a string of code to execute executable code, which is programmed to belong to the field element.

ProxyAPI allows us to create an intermediate layer in the object and its consumer entity, this feature provides us with the ability to control the object, for example, you can decide how to conduct it getand set, even when you can customize this visit does not exist on the object properties What can we do when

Proxy API

var p = new Proxy(target, handler);

ProxyThe constructor takes a targetsubject, and an interceptor to targettarget different behavior of handlerobjects. You can set the following interception items:

  • has- interception inoperations. For example, you can use it to hide certain properties on the object.
  • get— Used to intercept read operations. For example, when trying to read a non-existent property, you can use it to return the default value.
  • set— Used to intercept assignment operations. For example, you can add validation logic when assigning a value to the attribute, and an error can be thrown if the validation fails.
  • apply— Used to intercept function call operations. For example, you can put all the function calls are wrapped in a try/catchstatement block.

This is only part of the interception items, you can find the complete list on MDN .

The following is a simple example of using Proxy for authentication:

const Car = {
  maker:'BMW',
  year: 2018,
};

const proxyCar = new Proxy(Car, {
  set(obj, prop, value) {
    if (prop ==='maker' && value.length <1) {
      throw new Error('Invalid maker');
    }

    if (prop ==='year' && typeof value !=='number') {
      throw new Error('Invalid year');
    }
    obj[prop] = value;
    return true;
  }

});

proxyCar.maker ='';//throw exception
proxyCar.year = '1999';//throw exception

As you can see, we can use Proxy to verify the value assigned to the proxy object.

Use Proxy to debug

In order to demonstrate the capabilities of Proxy in practice, I created a simple monitoring library to monitor a given object or class. The monitoring items are as follows:

  • Function execution time
  • The caller of the function or the visitor of the property
  • Count the number of times each function or attribute has been accessed.

This is done by visiting any of the objects, classes, and even when a function is called a named proxyTrackfunction to complete.

This library is very useful if you want to monitor who assigns a value to an object's property, or how long a function has been executed, how many times it has been executed, and who has executed it. I know there may be other better tools to achieve the above functions, but here I created this library to use this API.

Use proxyTrack

1. let's see how to use:

function MyClass() {}

MyClass.prototype = {
    isPrime: function() {
        const num = this.num;
        for(var i = 2; i <num; i++)
            if(num% i === 0) return false;
        return num !== 1 && num !== 0;
    },

    num: null,
};

MyClass.prototype.constructor = MyClass;

const trackedClass = proxyTrack(MyClass);

function start() {
    const my = new trackedClass();
    my.num = 573723653;
    if (!my.isPrime()) {
        return `${my.num} is not prime`;
    }
}

function main() {
    start();
}

main();

If we run this code, the console will output:

MyClass.num is being set by start for the 1 time
MyClass.num is being get by isPrime for the 1 time
MyClass.isPrime was called by start for the 1 time and took 0 mils.
MyClass.num is being get by start for the 2 time

proxyTrackTwo parameters are accepted: the first is the object/class to be monitored, and the second is a configuration item object. If it is not passed, it will be set to the default value. Let's see what the default value of this configuration item looks like:

const defaultOptions = {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    stdout: null,
    filter: null,
};

As you can see, you can monitor your goals by configuring the monitoring items you care about. For example, you want to output the results come out, then you can console.logassignstdout .

Also can be assigned to filtercustomize what information to control the output of the callback function. You will get an object that includes monitoring information, and return if you want to keep this information true, and vice versa false.

Use proxyTrack in React

Because React component is actually like, so you can proxyTrackreal-time monitoring it to. such as:

class MyComponent extends Component{...}

export default connect(mapStateToProps)(proxyTrack(MyComponent, {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    filter: (data) => {
        if( data.type ==='get' && data.prop ==='componentDidUpdate') return false;
        return true;
    }
}));

As you can see, you can filter out the information you don't care about, otherwise the output will become messy.

Implement proxyTrack

Let's look at proxyTrackimplementation.

The first is the function itself:

export function proxyTrack(entity, options = defaultOptions) {
    if (typeof entity ==='function') return trackClass(entity, options);
    return trackObject(entity, options);
}

Nothing special, here is just calling related functions.

Look again trackObject:

function trackObject(obj, options = {}) {
    const {trackFunctions, trackProps} = options;

    let resultObj = obj;
    if (trackFunctions) {
        proxyFunctions(resultObj, options);
    }
    if (trackProps) {
        resultObj = new Proxy(resultObj, {
            get: trackPropertyGet(options),
            set: trackPropertySet(options),
        });
    }
    return resultObj;
}
function proxyFunctions(trackedEntity, options) {
    if (typeof trackedEntity ==='function') return;
    Object.getOwnPropertyNames(trackedEntity).forEach((name) => {
        if (typeof trackedEntity[name] ==='function') {
            trackedEntity[name] = new Proxy(trackedEntity[name], {
                apply: trackFunctionCall(options),
            });
        }
    });
}

We can see, if we want to monitoring the properties of an object, we created a with getand setmonitored objects interceptors. Here is the setrealization of the interceptor:

function trackPropertySet(options = {}) {
    return function set(target, prop, value, receiver) {
        const {trackCaller, trackCount, stdout, filter} = options;
        const error = trackCaller && new Error();
        const caller = getCaller(error);
        const contextName = target.constructor.name ==='Object'?'': `${target.constructor.name}.`;
        const name = `${contextName}${prop}`;
        const hashKey = `set_${name}`;
        if (trackCount) {
            if (!callerMap[hashKey]) {
                callerMap[hashKey] = 1;
            } else {
                callerMap[hashKey]++;
            }
        }
        let output = `${name} is being set`;
        if (trackCaller) {
            output += `by ${caller.name}`;
        }
        if (trackCount) {
            output += `for the ${callerMap[hashKey]} time`;
        }
        let canReport = true;
        if (filter) {
            canReport = filter({
                type:'get',
                prop,
                name,
                caller,
                count: callerMap[hashKey],
                value,
            });
        }
        if (canReport) {
            if (stdout) {
                stdout(output);
            } else {
                console.log(output);
            }
        }
        return Reflect.set(target, prop, value, receiver);
    };
}

Even more interesting is trackClassthe function (at least for me is this):

function trackClass(cls, options = {}) {
    cls.prototype = trackObject(cls.prototype, options);
    cls.prototype.constructor = cls;

    return new Proxy(cls, {
        construct(target, args) {
            const obj = new target(...args);
            return new Proxy(obj, {
                get: trackPropertyGet(options),
                set: trackPropertySet(options),
            });
        },
        apply: trackFunctionCall(options),
    });
}

In this case, because we want to intercept properties on this class that are not on the prototype, we created a proxy for the prototype of this class and created a constructor interceptor.

Don't forget, even if you define a property on the prototype, if you assign a property with the same name to this object, JavaScript will create a local copy of this property, so the change of the assignment will not change the other instances of this class behavior. This is why just acting as a proxy for the prototype does not meet the requirements.

Click here to view the complete code.

Reference: https://cloud.tencent.com/developer/article/1451719 [Translation] Use Proxy to monitor classes in Javascript-Cloud + Community-Tencent Cloud