Monthly Archives: January 2014

ProxyJS – A JavaScript Proxy Library

What’s A Proxy

My JavaScript-centric and simplified definition of proxy is: “it’s an object that functions as an interface to another function or method”. To act as an interface to another functions, a proxy wraps that function and intercepts all the calls that are made to it. This ability to intercept calls made to the wrapped function allows a proxy to spy on it.

Proxies Are Spies

For example, suppose you have to integrate a library into an application that you are building out and that library’s documentation isn’t all that clear. Ideally you would like to spy each call made to one or more of that library’s methods and record the arguments they are passed and what they return. In fact, that is exactly what we are going to focus on and build out in this tutorial, using proxy as a spy to gleam information about the functions they are wrapping.

Building A Small Proxy Library

The approach I will take to implementing proxy relies on the fact that in JavaScript functions are themselves objects and as such they can have their own member properties. We will see how this plays out in the examples as proxy gets incrementally built out in three stages:

  1. An infrastructure
  2. Recording information
  3. API

Identifying Requirements

Everything worth building deserves to have its requirements identified before a single line of code is written and is the case for ProxyJS, the proxy library that we will build here. So, simply stated, proxy must:

  1. Maintain a count of how many times the proxy was called.
  2. Record the arguments that are passed to it every time the proxy was called.
  3. Call the wrapped function using its intended context (the intended “this”) passing it the arguments that proxy itself was called with.
  4. Record what the wrapped function returns.
  5. Return what the wrapped function returns to the caller.
  6. Provide an API which allows retrieving the information that has been recorded.

Infrastructure Implementation

First, A naive Infrastructure Implementation

Lets first begin with what is admittedly a naive infrastructure implementation and create a function named unsurprisingly “proxy”. At first we’ll have it take a single argument, the function that is to be wrapped:


function proxy(fnArg){}

At this point a naive implementation of a suitable wrapper mechanism might appear to be


function proxy(fnArg){
    fnArg();
}

but this falls far short of our requirement as it really just provides one more additional level of indirection between the caller and the wrapped function. We need to build out an infrastructure that will support all our requirements as was laid out above.

Stage 1 – Infrastructure

To provide an API proxy’s implementation will require property methods which will be applied to the function that proxy returns to the caller. A note of caution, though, we have to safeguard that the function returned to the caller is isolated from other functions that are returned. In order to provide isolation we will use a modular pattern in combination with a factory method which is responsible for creating scope and closure around the function that is returned to the caller.


function proxy(){

    var proxyFactory = function(fnArg){

        var fnToCall = fnArg;
        var fn = function(){
            fnToCall();
        };
        return fn;
    };

    return proxyFactory(arguments[0]);

}

var targetFn = function(string){
    console.log("targetFn called with " + string + " and with conext of " + this);
};

var prxyFn = proxy(targetFn);
prxyFn("Somewhere over the rainbow");

We can now use proxy to call the wrapped function but when we run the above (in Chrome developer tools, for instance) we can immediately see that there is a problem – we haven’t accounted for the fact that the function we are wrapping might be called with arguments. The argument that we passed when we called prxyFn(“Somewhere over the rainbow”) was never passed to the wrapped function – it logs to the console “targetFn called with [object window]” (Note: if you use strict mode it will output “targetFn called with undefined”). In addition to not passing the arguments to the wrapped function, we also haven’t provided the ability to call the wrapped function with the intended context – the value of its “this”. Our implementation must allow for both of these, calling the wrapped function with all the arguments that were passed to it and calling it with the correct context.


function proxy(){

    var proxyFactory = function(fnArg){

        var fnToCall = fnArg;
        var fn = function(){
            var args = [].slice.call(arguments);
            fnToCall.apply(this, args);
        };
        return fn;
    };

    return proxyFactory(arguments[0]);

}

var targetFn = function(string){
    console.log("targetFn called with " + string + " and with conext of " + this);
};

var prxyFn = proxy(targetFn);
prxyFn("Somewhere over the rainbow");

Now, when we run the above in the console, we can see that the argument was passed to the wrapped function and that we also called the wrapped function with the intended context, which in this case happens to be the global window context since we are proxying a mere function. But what if we wanted to proxy a method of an object instead, and to be able to call that method directly on the object so that its calling context is correct? The next implementation addresses those concerns.


function proxy(){

    var proxyFactory = function(){

        var fnToCall = arguments.length === 2 ? arguments[0][arguments[1]] : arguments[0];
        var fn = function(){
            var args = [].slice.call(arguments);
            fnToCall.apply(this, args);
        };
        if(arguments.length === 2){
            arguments[0][arguments[1]] = fn;
        }
        return fn;
    };

    var args = [].slice.call(arguments);
    return proxyFactory.apply(null, args);

}

var targetFn = function(string){
    console.log("targetFn called with " + string + " and with conext of " + this);
};

var prxyFn = proxy(targetFn);
prxyFn("Somewhere over the rainbow");

var someObject = {
    targetFn: function(string){
        console.log("targetFn called with " + string + " and with conext of " + this);
    }
};
prxyFn = proxy(someObject, "targetFn");
someObject.targetFn("Somewhere over the rainbow");

With the above we have provided proxy the ability to not only wrap functions but also object property methods. Being able to proxy object property methods means that we can call those property methods through their objects and by doing so maintain the correct context of the method calls. When we run this code in the console we can see that both the function and the property methods were both called with arguments and both were also called with the correct context.

Stage 2 – Recording Information

With proxy’s infrastructure now in place lets turn our attention to the information we wish to record, namely maintaining a count of how many times proxy was called, the arguments that are passed to it and what it returned.


function proxy(){

    var proxyFactory = function(){

        var fnToCall = arguments.length === 2 ? arguments[0][arguments[1]] : arguments[0];
        //A counter used to note how many times proxy has been called.
        var xCalled = 0;
        //An array of arrays used to note the arguments that were passed to proxy.
        var argsPassed = [];
        //An array whose elements note what the wrapped function returned.
        var returned = [];
        var fn = function(){
            //Note the arguments that were passed for this invocation.
            var args = [].slice.call(arguments);
            argsPassed.push(args.length ? args : []);
            //Increment the called count for this invocation.
            xCalled += 1;
            //Call the wrapped function noting what it returns.
            var ret = fnToCall.apply(this, args);
            returned.push(ret);
            //Return what the wrapped function returned to the caller.
            return ret;
        };
        if(arguments.length === 2){
            arguments[0][arguments[1]] = fn;
        }
        return fn;
    };

    var args = [].slice.call(arguments);
    return proxyFactory.apply(null, args);

}

var targetFn = function(string){
    console.log("targetFn called with " + string + " and with conext of " + this);
    return string;
};
var prxyFn1 = proxy(targetFn);
prxyFn1("Somewhere over the rainbow");

var someObject = {
    targetFn: function(string){
        console.log("targetFn called with " + string + " and with conext of " + this);
        return string;
    }
};
prxyFn2 = proxy(someObject, "targetFn");
someObject.targetFn("Somewhere over the rainbow");

If we run the above and step through it with the debugger we can confirm that the code is indeed recording the information accurately. All that’s missing now is an API that will provide access to all that good information that proxy is accumulating for us.

Stage 3 – API

The API takes advantage of the fact that JavaScript functions are themselves first class objects. Therefore, each API method is implemented as a privileged function and is exposed to the caller as properties of the proxy object itself. This way if the user inadvertently assigns something to them it wont affect any other instances of proxy.


function proxy(){

    var proxyFactory = function(){

        //The wrapped function to call.
        var fnToCall = arguments.length === 2 ? arguments[0][arguments[1]] : arguments[0];

        //A counter used to note how many times proxy has been called.
        var xCalled = 0;

        //An array of arrays used to note the arguments that were passed to proxy.
        var argsPassed = [];

        //An array whose elements note what the wrapped function returned.
        var returned = [];

        ///
        ///Privileged functions used by API
        ///

        //Returns the number of times the wrapped function was called.
        var getCalledCount = function(){
            return xCalled;
        };

        //If called with 'n' and 'n' is within bounds then returns the
        //array found at argsPassed[n], otherwise returns argsPassed.
        var getArgsPassed = function(){
            if(arguments.length === 1 && arguments[0] >= 0 && arguments[0] < argsPassed.length){
                return argsPassed[arguments[0]];
            }else{
                return argsPassed;
            }
        };

        //If called with 'n' and 'n' is within bounds then returns
        //value found at returned[n], otherwise returns returned.
        var getReturned = function(){
            if(arguments.length === 1 && arguments[0] >= 0 && arguments[0] < returned.length){
                return returned[arguments[0]];
            }else{
                return returned;
            }
        };

        //If 'n' is within bounds then returns an
        //info object, otherwsie returns undefined.
        var getData= function(n){
            if(n >= 0 && n < xCalled){
                var args = getArgsPassed(n);
                var ret = getReturned(n);
                var info = {
                    count: n + 1,
                    argsPassed: args,
                    returned: ret
                };
                return info;
            }
        };

        //A higher order function - iterates through the collected data and
        //returns the information collected for each invocation of proxy.
        var dataIterator = function(callback){
            for(var i = 0; i < xCalled; i++){
                callback(getData(i));
            }
        };

        //The function that is returned to the caller.
        var fn = function(){
            //Note the arguments that were passed for this invocation.
            var args = [].slice.call(arguments);
            argsPassed.push(args.length ? args : []);
            //Increment the called count for this invocation.
            xCalled += 1;
            //Call the wrapped function noting what it returns.
            var ret = fnToCall.apply(this, args);
            returned.push(ret);
            //Return what the wrapped function returned to the caller.
            return ret;
        };

        ///
        ///Exposed Lovwer level API - see Privileged functions used by API above.
        ///

        fn.getCalledCount = getCalledCount;

        fn.getArgsPassed = getArgsPassed;

        fn.getReturned = getReturned;

        fn.getData = getData;

        ///Exposed Higher Order API - see Privileged functions used by API above.
        fn.dataIterator = dataIterator;

        ///Replaces object's method property with proxy's fn.
        if(arguments.length === 2){
            arguments[0][arguments[1]] = fn;
        }

        //Return fn to the caller.
        return fn;
    };

    //Convert arguments to an array, call factory and returns its value to the caller.
    var args = [].slice.call(arguments);
    return proxyFactory.apply(null, args);

}

var targetFn = function(string){
    console.log("targetFn called with " + string + " and with conext of " + this);
    return string;
};
var prxyFn1 = proxy(targetFn);
prxyFn1(1);
prxyFn1(2);
prxyFn1(3);
prxyFn1(4);
prxyFn1(5);
prxyFn1(6);
prxyFn1(7);
prxyFn1(8);
prxyFn1(9);
prxyFn1.dataIterator(function(info){
    console.log(info);
});

var someObject = {
    targetFn: function(string){
        console.log("targetFn called with " + string + " and with conext of " + this);
        return string;
    }
};
prxyFn2 = proxy(someObject, "targetFn");
someObject.targetFn(1);
someObject.targetFn(2);
someObject.targetFn(3);
someObject.targetFn(4);
someObject.targetFn(5);
someObject.targetFn(6);
someObject.targetFn(7);
someObject.targetFn(8);
someObject.targetFn(9);
someObject.targetFn.dataIterator(function(info){
    console.log(info);
});

The API itself has four lower level functions, getCalledCount, getArgsPassed, getReturned and getData. Both getArgsPassed and getReturned can be called with or without arguments. If they are called without arguments they just return their backing variables, which are arrays. If they are called with a number which is in bounds then their backing variables’ corresponding array elements are returned. getData must be called with a number which is in bounds and returns an info object with count, argsPassed and returned properties. The API’s one higher level method dataIterator, which internally calls getData, takes a callback function as its only parameter which it calls with the info object returned by getData for as many times as proxy was invoked.

Summary

ProxyJS is now complete and fully functional. The source code for ProxyJS is available in a Gist on GitHub at https://gist.github.com/jeffschwartz/8375965. Please feel free to download it, play with it and to extend it for your own needs.

On a personal note, I am often amazed at what this little “toy language” JavaScript, yes, the language that some love to hate, is capable of. If you think about the functionality that is implemented into proxy and then consider how little code it actually took to implement it, well I find that impressive and I hope you do also. JavaScript sometimes just takes my breath away and leaves me speechless.

Please don’t forget to leave comments and remember to come back soon because I have a list of very interesting JavaScript topics that I intend writing about. Until then, happy coding!