• Using Proxy to Track Javascript Class
  • Amir Harel
  • Translation from: Aliyun Translation Group
  • Text link: github.com/dawn-teams/…
  • Find Tong
  • Proofreader: Yishu

Proxy objects are a cool and little-known feature of ES6. Although it has been around for quite some time, I wanted to write this article and explain what it does, with a real-world example of how to use it.

What is the Proxy

As defined on the MDN website:

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, The Enumeration, Function Invocation, etc.).proxy object is used to define custom behavior for basic operations (such as property lookup, assignment, enumeration, function invocation, etc.).

Although this is almost a comprehensive summary, when I read it, I’m not quite sure what it does or how it helps.

First, the concept of Proxy comes from the world of meta-programming. Simply put, metaprogramming is code that allows us to use the application (or core) code we write. For example, the infamous eval function, which allows us to evaluate string code as executable code, belongs in the realm of metaprogramming.

The Proxy API allows us to create a layer between an object and its consuming entity, which allows us to control the behavior of that object, such as deciding how to perform the behavior of GET and set, or even what to do when someone tries to access an undefined attribute in the object.

Proxy API

var p = new Proxy(target, handler);
Copy the code

The Proxy object gets target and handler to capture different behaviors in the target object. Here are some of the traps you can set:

  • hasCapture –inOperators. For example, this will allow you to hide certain attributes of an object.
  • getCapture –Gets the value of an attribute in an objectOperation. For example, if this property does not exist, this will allow you to return some default values.
  • setCapture –Sets the value of a property in an objectOperation. For example, this will allow you to validate a value set as a property and throw an exception if the value is invalid.
  • apply– Capture function calls. For example, this will allow you to wrap all functions intry/catchCode block.

This is a simple example, but you can see the full list on the MDN website.

Let’s look at a simple example of authentication using Proxy:

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
Copy the code

As you can see, we can validate the values we are setting into the proxy object.

Debugging with a Proxy

To show the power of Proxy, I created a simple trace library that tracks the following for a given object/class:

  • The execution time of the function
  • The caller of each method or property
  • The number of calls per method or property

It is implemented by calling proxyTrack on any object, class, or even function.

This can be useful if you want to know who is changing a value in an object, or how long and how many times a function is called, and who is calling it. I know there are probably better tools for doing this, but I created this library just to try out the Proxy API.

Using proxyTrack

First, let’s see how to use it:

function MyClass() {}

MyClass.prototype = {
    isPrime: function() {
        const num = this.num;
        for(var i = 2; i < num; i++)
            if(num % i === 0) return false;
        returnnum ! == 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();
Copy the code

If we execute this code, we’ll see on the console:

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
Copy the code

ProxyTrack passes two arguments: the first is the object/class to track, and the second is option, which will be set to default option if not passed. Let’s look at the second parameter:

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

As you can see, you can control what to track by setting the appropriate flags. If you want to control the output to go elsewhere first and then to console.log, you can pass a function to stdout.

You can also control which trace message is output if you pass in the Filter callback function. You will get an object that contains trace data information and must return true to keep the message, or false to ignore it.

Use proxyTrack in React

The React component is actually a class, so you can trace the class to examine it in real time. 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; }}));Copy the code

As you can see, you can filter out messages that might not be relevant to you, or might clutter the console.

The realization of the proxyTrack

Let’s look at how the proxyTrack method is implemented.

First, the function itself:

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

There’s nothing special here, we’re just calling the corresponding function.

Look at the 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), }); }}); }Copy the code

As you can see, if we need to track the properties of an object, we create a proxy object using get and set traps. Here is the code to set the trap:

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);
    };
}
Copy the code

The trackClass function is more interesting to me:

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

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

In this case, we want to create a proxy for the function prototype and a trap for the constructor, because we want to be able to capture properties in the class that are not from the prototype.

Don’t forget that even if you define a property at the stereotype level, once you set a value for it, JavaScript creates a local copy of that property, so all other instances of the class don’t change with it. That’s why it’s not enough to just proxy the prototype.

You can see the full code here.