Tuesday, 12 April 2016

JavaScript: getting my brain further around bind()

G'day:
I was refactoring some JS code today, and one of the refactorings was extracting an inline callback into its own separate function. This was the "success" callback for a jQuery AJAX request. The reason I wanted to pull it out of its inline situation was twofold:

  1. the function it was in was already way too long;
  2. it itself was a bit on the long side, and had a few "moving parts", and I wanted to extract it so it was testable.
I hasten to add we don't have any JS testing in place yet, but I like our code to be positioned to be testable for making our lives easier once we bring it in. It occurs to me now that this is an example of premature optimisation in a way, but the remit of the work I was doing was precisely this, so I figure it's OK. What's more... I've currently got time to do this, and later on when someone comes to need to do maintenance on this code they might not have time to wade through layers of nested complicated logic, so now is a good time to tidy up a bit.

I can't use our actual code to demo what's going on here, but I've done a reduced proof of concept which demonstrates the point without all the distracting baggage.

The existing situation was this sort of thing:

var usingInline = function(firstThree, fourth, type){
    var allFour = firstThree;
    allFour.push(fourth);
    var prefix = type;
    allFour.forEach(function(element){
        console.log(prefix + ": " + element);
    });
};

var oneTwoThreeInMaori = ["tahi", "rua", "toru"];
var fourInMaori = "wha";
usingInline(oneTwoThreeInMaori, fourInMaori, "Number");

That code will run as it stands, and outputs:

Number: tahi
Number: rua
Number: toru
Number: wha


No AJAX call here, but there's an inline callback buried in the middle of it. Truth be told with a callback this small, I'd just leave it there in real life, but that would make for a short, pointless blog article. My task here is to extract that callback.

As is my occasional wont, I did not pay attention to what I was doing and just ripped the callback out into its own function, and replaced the inline call to it with the name of the new function:

var usingExtracted = function(firstThree, fourth, type){
    var allFour = firstThree;
    allFour.push(fourth);
    var prefix = type;

    allFour.forEach(eachHandler);
};

var eachHandler = function(element){
    console.log(prefix + ": " + element);
};

var oneTwoThreeInMaori = ["tahi", "rua", "toru"];
var fourInMaori = "wha";
usingExtracted(oneTwoThreeInMaori, fourInMaori, "Number");

And that goes... splat:

Uncaught ReferenceError: prefix is not defined(…)

Why? Cos in the first example we were leveraging closure: the function expression was declared in the context of the usingInline function, so it enclosed the prefix variable, so it was available to use within the callback code. Closure in action. Whereas with the second example, eachHandler is declared in the global document context (well: it was within a class in the original code, but same thing applies: it was a different context from that which prefix was defined), so prefix was not there to close over, so it was not available.

Duh.

Prior to a week or so ago, my way of handling this would be to still declare the handler separately (so it's still testable), but instead of calling it directly, wrapping it in an inline function which does nothing but wraps a call to it, in the process enclosing prefix, and passing it into the handler. Like this:

var usingWrapper = function(firstThree, fourth, type){
    var allFour = firstThree;
    allFour.push(fourth);
    var prefix = type;

    allFour.forEach(function(element){
        eachHandlerToUseWithWrapper(prefix, element);
    });
};

var eachHandlerToUseWithWrapper = function(prefix, element){
    console.log(prefix + ": " + element);
};

Note that the handler now takes a prefix argument too, instead of just the argument forEach() passes to its callback.

That's all good. It's a bit "Heath Robinson", but it works.

A few weeks ago I wrote that article "JavaScript: clarifying in my head this and that and proxies" about proxying this in JavaScript callbacks. One of the techniques I looked at was using bind() to bind a different this to a function. One of the things I did not investigate but recalled reading about was that it can also be used to pass additional arguments into the function it returns. This is exactly what I want, and it was quick to knock together:


var usingExtractedWithBind = function(firstThree, fourth, type){
    var allFour = firstThree;
    allFour.push(fourth);
    var prefix = type;

    allFour.forEach(eachHandlerToUseWithBind.bind(undefined, prefix));
};

var eachHandlerToUseWithBind = function(prefix, element){
    console.log(prefix + ": " + element);
};

Here I'm not interested in proxying this as I don't need it in the callback, so I just leave it undefined (this was from the docs, and seems reasonable, but I would not have guessed to do this), and simply specify prefix as a value to pass into the function bind() creates. Then I use the same callback as before with both the prefix argument, and the element that forEach() passes in. What bind() is doing here is basically the same as in my previous example: it's making a new function, which receives the prefix value as its first argument, and then receives whatever other args are passed to it. Cool!

Perhaps not earth-shattering for you lot, but this was the first time I put this into action, so it's kinda the more interesting "TIL" from today (I had a few today actually... but I'll get to them one at a time).

There's a working JSFiddle of this code here.

Righto.

--
Adam