JavaScript Proxies

A Proxy is a powerful tool, it lets you do some amazing things. You could make validations, tracking called functions, or even spy on them.

When testing, you can use jest.spyOn(...) and you can control what's happening in that function or object. But what if we want to do this on code that is not in a testing environment?

Proxies to the rescue! A Proxy is an object that lets us define custom behavior for fundamental operations. For instance, apply, set, get...

You can find more information about Proxies here. Keep in mind Proxies are not compatible with Internet Explorer and you can't polyfill them. So if you are going to use them, it has to be on modern browsers.

Imagine we want to keep track of how many times a function is being called and we also want to know which arguments it's being called with. Its easier to do this on functions that are in objects. So we can replace them easily.

In the example below, we can keep track of every React.createElement called and all its arguments. So we create a Map and we store there every call with a timestamp.

const myStore = new Map();
const handler = {
  apply(target, thisArg, argumentsList) {
    myStore.set(`${target.name}-${Date.now()}`, argumentsList);

    return Reflect.apply(target, thisArg, argumentsList);
  },
};

React.createElement = new Proxy(React.createElement, handler);

We should add this before our application runs and any time we can check what happened until that moment. In the end, we have all calls to this function and we have our map with the timestamps and every argument.

We could do this with any object with functions we have. But what about any other function?

Well, this one is more tricky, a Proxy gives you an exact copy of the function you give to it. The problem is that you have to call the "proxied" function and not the original one.

So in the following case:

function mySuperSumFunction(a, b) {
 return a + b;
}

We could do this

const handler = {
  apply(target, thisArg, argumentsList) {
    console.log(`${target.name}-${Date.now()}`, argumentsList);

    return Reflect.apply(target, thisArg, argumentsList);
  },
};

mySuperSumFunction = new Proxy(mySuperSumFunction, handler);

This is not something you should have in your code, but it can be a nice temporary way to track whats going on.

What if we need to do this in classes? Well, first of all, I will assume you know classes are only syntactic sugar and under the hood is still the same prototype inheritance. So in this case

class MyClass {
  a(x) {
    return x + 1;
  }
  static myStaticFunction(x) {
    return x - 1;
  }
}

We could get control over all these functions by doing this

// Your handler code ...

MyClass.prototype.a = new Proxy(MyClass.prototype.a, handler);
MyClass.myStaticFunction = new Proxy(MyClass.myStaticFunction, handler); 

With the following code, we could "proxy" every function in a class, static or non-static functions.

function addProxyOnFunctions(obj, handler) {
  Object.getOwnPropertyNames(obj.prototype).forEach(prop => {
    if (obj.prototype[prop].call) {
      obj.prototype[prop] = new Proxy(obj.prototype[prop], handler);
    }
  });

  Object.getOwnPropertyNames(obj).forEach(prop => {
    if (obj[prop].call) {
      obj[prop] = new Proxy(obj[prop], handler);
    }
  });
}

But we still have a problem... What about function expressions?

const mySuperSumFunction = function(a, b) {
 return a + b;
}

We have a problem here, we can't override a const 😟. So what is the solution to this?

Well, I don't have a nice solution for this, the only way I know how to do it is by changing const for let and we will be able to override it.

let mySuperSumFunction = function(a, b) {
 return a + b;
}

mySuperSumFunction = new Proxy(mySuperSumFunction, handler);