OdeToCode IC Logo

Metaprogramming Fun In JavaScript

Tuesday, September 3, 2013

The idea is to take a JavaScript statement like the following:

c.find({ x: 1, y: 3, name: "foo" }, { id: 0 }).limit(1);

.. and turn the statement into a data structure that describes the methods being invoked and the arguments for each method call. There are multiple method names to capture (not just find and limit, but also findOne, orderBy, and more).

This sounds like a job for a mock object library, but let’s explore a few simple approaches that use < 20 lines of code.

One approach that doesn’t work is try to to build a function for each possible method call and forget how closures work. The following code has a bug.

var CommandCapture = function () {

    var commands = ["find", "findOne", "limit", "orderBy"];

    for (var i = 0; i < commands.length; i++) {
        this[commands[i]] = function() {
            return {
                name: commands[i], // here's the problem
                args: arguments
            };
        };
    }
};

By the time the innermost function executes, the value of i is outside the bounds of the commands array, so the name isn’t properly captured.

To avoid the closure problem we can embed the command name into a string and use the Function constructor.

var CommandCapture = function() {
    
    var commands = ["find", "findOne", "limit", "orderBy"];
    
    for (var i = 0; i < commands.length; i++) {
        this[commands[i]] = new Function("return { name: '" +
                                            commands[i] +
                                         "', args:arguments };");
    }
};

But ... building functions out of strings is tiresome and error prone, so instead we can use an IFFE and capture the value of i in a more proper manner.

var CommandCapture = function () {

    var commands = ["find", "findOne", "limit", "orderBy"];

    for (var i = 0; i < commands.length; i++) {
        this[commands[i]] = function(name) {

            return function() {
                return {
                    name: name,
                    args: arguments
                };
            };
            
        }(commands[i]);
    }
};

The above code will work for simple cases. Executing the following code:

var capture = new CommandCapture();
var result = capture.find({ x: 1, y: 3, name: "foo" }, { id: 0 });
console.log(result);

Yields this output:

{ 
    name: 'find',
    args: { '0': { x: 1, y: 3, name: 'foo' }, '1': { id: 0 } }
}

However, the above code doesn’t allow for method chaining (capture.find().limit()). The following code does, by keeping all the method calls and arguments in a field named $$captures.

var CommandCapture = function () {

    var self = this;
    self.$$captures = [];
    var commands = ["find", "findOne", "limit", "orderBy"];

    for (var i = 0; i < commands.length; i++) {
        this[commands[i]] = function (name) {

            return function () {
                self.$$captures.push({ name: name, args: arguments });
                return self;
            };

        }(commands[i]);
    }      
};

These are the types of problems that are fun to work on during a Saturday afternoon thunderstorm.