Functional JavaScript Wizardry
Since EcmaScript 5, JavaScript has gotten nice collection functions
like Array.map, Array.forEach, Array.filter and the
usual suspects. However, they can be a bit clunky to use
when dealing with objects. Look at the following piece of code:
[" fun", " ction ", " al"].map(function(str) { return str.trim(); })
.join("");
//=> 'functional'I map over an array of strings, trim each string, then join the resulting array of trimmed strings. Except it’s a bit too verbose for my tastes. I would like to write (and read) this instead:
[" fun", " ction ", " al"].map(trim).join("");The straightforward way to do that is to define the anonymous wrapper
function that calls trim beforehand:
var trim = function(str) { return str.trim(); };
[" fun", " ction ", " al"].map(trim).join("");
//=> 'functional'Okay. Except that I don’t want to call just trim, I might call any
function on Array, or Object, or any custom created object for
that matter. And I certainly don’t want to write stupid wrapper
functions each time.
The trim function already has a name: String.prototype.trim. But
unfortunately, I cannot just write the following:
[" fun", " ction ", " al"].map(String.prototype.trim).join("");
// TypeError: String.prototype.trim called on null or undefinedBecause here map will call trim on each string in the array, but
without any context object (a value for this). Instead, I would
like map to pass each string as the context object to trim, in
turn. We could redefine a custom version of map that works that
way, but there’s a simpler solution.
Abstracting wrappers
What we really need is a function that transforms method calls into
function calls. We need a function m2f to transform o.m(args)
calls into f(o, args) calls, where f = m2f(o.m). Easy enough
in JavaScript (bar the ugliness of dealing with the arguments special
object):
function m2f(fun) {
return function(/* args */) {
var args = [].slice.apply(arguments);
return fun.apply(args.shift(), args);
};
}
var trim = m2f(String.prototype.trim);
[" fun", " ction ", " al"].map(trim).join("");
//=> 'functional'Good! Now I can use m2f on any “method”, and get back a more
flexible function. For instance, I can use map on any “array-like”
objects like strings, the special arguments object, NodeList and
so on:
var map = m2f([].map); // Saving a few chars over `Array.prototype.map`
var up = m2f(String.prototype.toUpperCase);
map("abc", up)
// [ 'A', 'B', 'C' ]
map({0: 'albatros', 1: 'albatros', length: 2}, up);
// [ 'ALBATROS', 'ALBATROS' ]
function(){ return map(arguments, up) }(1, 2, 'abc', 'z')
// [ '1', '2', 'ABC', 'Z' ]And of course, I can use m2f on any method, not just map. Here I
populate an array object with functions from Array.prototype that
work on any array-like object:
var array = {};
function isFunction(x) { return typeof x === 'function'; }
var names = Object.getOwnPropertyNames(Array.prototype);
names.forEach(function(name) {
if (isFunction(Array.prototype[name]))
array[name] = m2f(Array.prototype[name]);
})
array
// { join: [Function],
// pop: [Function],
// ... }
array.slice([0,1,2], -1)
//=> [ 2 ]This replicates the Mozilla-specific “Array generics” extension. This extension is not available in V8-powered environment like nodejs.
This m2f function is quite handy when you are used to a functional
style of programming. You can find it in Fogus’
Functional JavaScript under the name ‘invoker’. However,
there is another way to define m2f using a powerful corner of
EcmaScript 5.1, bind.
Calling and binding functions
Earlier, I said I could not just write:
var trim = String.prototype.trim;Because JavaScript methods are really just functions. Now my trim
is pretty useless on its own, because it expects a context object.
trim(" a ");
// TypeError: String.prototype.trim called on null or undefinedThere’s a way to provide one to it, using call:
trim.call(" a ");
// 'a'The call function takes a context, some arguments, and calls the
receiver (a function) with them.
Another way to provide the context is to use the bind function. As
its name implies, bind will return a function with the supplied
context bound to it. Let’s see how it works:
var trimA = trim.bind(" a ");
trimA();
// 'a'
trimA.call("b");
// 'a'Whenever trimA is called, it will call trim with "a" as context,
even if trimA is supplied another context with call.
Binding call
Where does that lead us? Well, if we look at the precedent example again,
trim.call(" a ");we see that trim.call is essentially the function we want. But we’d
rather use trim directly, without calling call on it. How to
achieve that?
Notice that call is itself a method call. So we can extract it on
its own, and use it to call trim. But since it’s an extracted
method, we still need to use call on it,
var call = trim.call;
call(trim, " a ");
// TypeError: object is not a function
call.call(trim, " a ");
// 'a'Since we are calling call on itself, the notation is a bit
overloaded, but it works. Note that now, trim is just an argument;
we can put any other method in there,
call.call([].map, "abc", up);
// [ 'A', 'B', 'C' ]In fact, there’s nothing tying call to trim. We could have
defined call without trim, by taking it on Function.prototype
directly,
var call = Function.prototype.call;Nevertheless, the greatest benefit of this transformation is that we
can now use bind to fix the first argument of call to a specific
function.
var map = call.bind([].map);
map("abc", up);
// [ 'A', 'B', 'C' ]And we just redefined m2f!
var m2f = call.bind;
var map = m2f([].map);
// TypeError: Bind must be called on a functionExcept it doesn’t work … because we did not provide a context to
bind. Putting it all together,
var call = Function.prototype.call;
var bind = call.bind;
var m2f = bind.bind(call);Alternatively, since bind is not tied to call, it might be clearer
to just write:
var call = Function.prototype.call;
var bind = Function.prototype.bind;
var m2f = bind.bind(call);Which finally gives this one-liner gem:
var m2f = Function.prototype.bind.bind(Function.prototype.call);A very useful tool for writing terse JavaScript.
A variant of this form was given by Dave Herman and explained by Erik Kronberg, which I read thrice without really understanding what was going on. This article is my attempt to derive this magic one-liner in a way that makes sense for me.