Jun. 19, 2010 at 12:22pmUnderscore Trick

JavaScript Callbacks with Arguments

I had an idea today while coding something for a personal project. Sometimes in JavaScript, you need to pass arguments to a function that you want to use as a callback. Imagine passing a callback to a click event where you want a state variable included. Well, you'd need to create a callback wrapper, until now.

Update (2010-06-22)

I've had a log of feedback on this idea. It turns out there are much better was of doing this. Most suggestions have been towards a functional programming technique called bind and curry. They do exactly the same thing. A commenter on Reddit pointed me to a functional library that does exactly this. Thank you to everyone for their feedback!


function _(func) {}

I propose using the _ (underscore) function as a wrapper for passing arguments to a callback. Here's what the code would look like:

function _(func) {
    var args = Array.prototype.slice.call(arguments, 1),
        self = (this === window ? undefined : this);
    return function () {
        func.apply(self || this, args);
    }
}
Yes, this is a bit of complicated, obtuse code. And it's use is not obvious. So here's a quick example:
function hide(elem, speed) {
    $(elem).slideUp(speed);
}
$('.button').click(function () {
    hide(this, 500);
});
Let's step through the example. The hide function takes in an element and a speed, then slides up that element. Every time you want to use your hide function as a callback, you have to wrap it with another function.

This is a pain in the butt! If you are nesting callbacks, your code becomes heavily indented. There have been cases where scoping becomes an issue. And, worse, rarely are you doing anything that justifies writing out the whole function declaration: you just want to invoke your callback, right? This is where the underscore trick comes in. Here is the same example rewritten with the underscore:
function hide(speed) {
    $(this).slideUp(speed);
}
$('.button').click(_(hide, 500));
The major difference is that your callback goes to the underscore function. The underscore function returns the callback that you would have written out by hand, only now you can passing arguments!

How about another example? This one is pause/unpause functionality of a slideshow.

Before

(function ($) {
    var paused = false;
    var speed = 750;
    var timeout = 10000;
    var timer;

    function pause(setTo) {
        paused = (setTo === undefined ? !paused : !!setTo);
        if(paused) {
            clearInterval(timer);
        } else {
            timer = setInterval(function () {
                swap('#slideshow', '.slides', '.current');
            }, timeout);
        }
    }

    function swap(wrapper, slides, current) {
        var $slides = $(wrapper).find(slides);
        var $curr = $slides.filter(current);
        var $next = $curr.next();
        if($next.length == 0) {
            $next = $slides.first();
        }
        $curr.fadeOut(speed, function () {
            $next.fadeIn(speed);
        });
    }

    function init() {     
        $('#slideshow').hover(function () {
            pause(true);
        }, function () {
            pause(false);
        });

        $('#pause').click(function () {
            pause();
        });
    }

    $(init);
}(window.jQuery));
Without rewriting the swap function, we can simplify the code to look more like the business logic.

After

(function ($) {
    var paused = false;
    var speed = 750;
    var timeout = 10000;
    var timer;

    function pause(setTo) {
        paused = (setTo === undefined ? !paused : !!setTo);
        if(paused) {
            clearInterval(timer);
        } else {
            timer = setInterval(_(swap, '#slideshow', '.slides', '.current'), timeout);
        }
    }

    function swap(wrapper, slides, current) {
        var $slides = $(wrapper).find(slides);
        var $curr = $slides.filter(current);
        var $next = $curr.next();
        if($next.length == 0) {
            $next = $slides.first();
        }
        $curr.fadeOut(_.call($next, $next.fadeIn, speed));
    }

    function init() {
        $('#slideshow').hover(_(pause, true), _(pause, false));
        $('#pause').click(_(pause));
    }

    $(init);
}(window.jQuery));
Notice how much shorter the init() function is with the underscore trick. Also, the interval inside the pause function is shorter and easier to read. When working with functions that require a specific use of the this variable, use the _.call() method. This is required when using jQuery specific methods, such as the $.fadeOut() above.

This trick has become my preference for managing callbacks. I've found that a callback which requires complicated logic is usually best to move it into it's own function. This trick allows that function to be written as though it is invoked directly, when it is really invoked as a callback. The real way to gauge the power of this trick is by using it. If you come up with any clever uses, let us know in the comments!

Interesting idea! I think your argument encapsulation strategy makes sense.

You may want to name the function something else though - there's a project called underscore js which seeks to compliment jQuery's $ function. I'm not sure what all underscore provides, but maybe this bit of logic would be a good fit there?

Anyway, great stuff!

Left by Jim R. Wilson | Jun. 21, 2010 at 11:07pm

I think _.bind() may meet your criteria: http://documentcloud.github.com/underscore/#bind

Left by Jim R. Wilson | Jun. 21, 2010 at 11:10pm

I am marking this in my google notebook. Thanks for the tip, I hope to use it soon. I wonder about performance though.

Left by Bobby | Jun. 22, 2010 at 12:32pm

I like this. It's similar to dojo.hitch and dojo.partial (http://docs.dojocampus.org/dojo/partial http://docs.dojocampus.org/dojo/hitch), but more consice.

Left by JustinJohnson.org | Jun. 24, 2010 at 11:45am

Leave a Comment

Remember me

Name:

Email:

URL:

Comment: * No HTML, http:// will auto-link
* required
Comment Guidelines