Higher-order Functions
Let's say that you were allergic to for loops--you don't like the syntax, maybe, or you dislike having to track a separate variable just for loop position. Could we write a function that does looping for us? First, let's write a function that just adds one to every item in an array that we pass in as the first argument.
var loop = function(array) {
for (var i = 0; i < array.length; i++) {
array[i] = array[i] + 1;
}
}
That works for one particular loop, but we don't want to have to write a new function for every loop--that defeats the purpose. Instead, we want a general purpose function, where we can "fill in" the operation on each item.
To solve this problem, remember that functions in JavaScript are also values, just like objects, numbers, strings, and booleans. They can be passed as arguments to a function--in fact, we've done so many times, every time we've added an event listener to an object. Here's an updated version of our loop function, but this time it will accept a function as input and call that function on each item in the array.
var loop = function(array, func) {
for (var i = 0; i < array.length; i++) {
array[i] = func(array[i], i);
}
}
Inside the loop, we're calling func with two arguments: the item value, and its position in the array. To use our new loop function, we just have to call it and pass in a function that will accept those two values, while returning the new value. Here's our add function again, rewritten into our generic loop form.
loop(function(item, i) {
return item + 1;
});
Tada! We've written a loop, but there's not a for in sight. Instead, it is isolated away inside of loop. Effectively, the function we pass in becomes the inner section of the loop.
In this case, loop is what we call a higher-order function--it's a function that either operates on or returns other functions. The style of using functions this way is called (appropriately enough) functional programming, and it's extremely useful for several reasons. It helps to isolate variables, creates reusable units of operation, and (for some people) is less "noisy" and more readable than the original loop form.
The Usual Suspects
The function we've made above is more commonly known as an "each" or "forEach" operation. In most browsers (including IE9 and up), arrays have a forEach built in, like so:
list.forEach(function(item) {
return item + 1;
});
In addition to forEach, there are a number of other common functional operations that come built-in for working with arrays. The following list includes a short description of each, as well as a small example of use.
- Map: map is similar to forEach, but it creates a new array instead of altering the existing values. You can think of it as "mapping" values in one array to the values in the next.
var input = [1, 2, 3]; var output = input.map(function(item) { return item * item; }); //output is [1, 4, 9], input is unchanged
- Reduce: It's often useful to be able to turn an array into a single value--say, to find the sum of its items, or to locate one particular item. reduce is a functional way to perform that selection, although it's a little confusing. In addition to the reduction function, you'll need to pass in a starting value. The function you pass in will get the following arguments: the result of the previous operation (or the starting value), the current value, the index of the current value, and the array itself. The following code adds up all the numbers in an array.
var input = [1, 2, 3, 4]; var sum = input.reduce(function(previous, current) { return previous + current; }, 0); //sum is 10
- Filter: This function does exactly what it sounds like. If the function you pass in returns something truthy, the value will be placed in the result array. Otherwise it will be discarded. This code, for example, returns an array that's filtered down to just names with fewer than four letters.
var names = ["Alice", "Bob", "Carl", "Dave", "Eve"]; var short = names.filter(function(name) { return name.length < 4; }); //short is ["Bob", "Eve"]
- Every and some: These two functions serve as counterparts to each other, as a kind of true/false reduce. every returns true if every item in the array passes the conditional function you provide. some will return true if at least one does.
var input = [1, 5, 9, 10] var allLessThan9 = input.every(function(item) { return item < 9; }); var someLessThan9 = input.some(function(item) { return item < 9; }); //allLessThan9 is false //someLessThan9 is true
- Sort: Unlike the other functions, which create new arrays with their results, sort operates in-place on the array. The comparison function that's passed in should take two values, and it return a number. If the number is less than zero, then the second argument should come first in the sorted array. If it's positive, the first argument comes first. If it's zero, they're the same value.
var names = ["Dave", "Bob", "Alice", "Eve", "Carl"]; names.sort(function(a, b) { if (a < b) { return 1; } if (a > b) { return -1; } return 0; });
Browser Compatibility
Rarely, of course, do most people use all these functions. The most common functional operations by far are each, map, and filter. Of course, if you're in an older browser like IE8 or below, you don't have even those available. Luckily, jQuery provides its own versions of them, albeit slower and with a few quirks.
$.each() can be called on both objects and arrays, unlike the built-in function which is only defined for arrays. It also swaps the order of arguments, with the index coming first and the item value second (these are different from the built-ins, because jQuery actually added its each before the browsers did). Because each is defined on the jQuery object, and not on arrays, you need to pass in the array you want to loop over as the first argument.
$.each(["A", "B", "C"], function(i, item) {
console.log(i, item); //logs out the index of each item, followed by its value
});
jQuery's map function doubles as its version of filter. If you return null from the input function, that item will be removed from the resulting array--this is a handy property that would actually be nice in the regular map function. Like $.each, $.map also works on both arrays and objects (although it only returns arrays), and must take its target as the first argument, followed by the actual function that will do the mapping/filtering.
var pets = {
dog: "Wallace",
cat: "Neko",
hamster: "Nadia"
}
var startWithN = $.map(pets, function(prop, name) {
var letter = /^N/; //find names that start with N
if (letter.test(name)) {
return name;
} else {
return null; //filter out non-N names
}
});
//startWithN = ["Neko", "Nadia"];
Between these two functions, you have most of the necessary tools for working with lists using functions. If you find that you want more functional power, but you need to be compatible with older browsers, you may want to check out Underscore.js, a library that includes not just each, map, reduce, and filter, but a dizzying arry of other functions for working with arrays.
Call (and Apply) Me Maybe
Once you get used to the idea that functions are just values to be passed around and called by other functions, it opens up an enormous range of potential applications. In fact, most of the powerful ideas in JavaScript come from the flexibility of these first-class functions. For example, let's say that you wanted to be able to delay the execution of a function. You could call setTimeout every time, but that gets cumbersome. Instead, let's write a function that wraps our original function in a delay of our choosing.
var wait = function(f, delay) {
return function() {
setTimeout(f, delay);
};
};
//now, we can create a delayed version easily:
var logHello = function() {
console.log("Hello");
};
var delayedHello = wait(logHello, 1000);
//call the returned function
delayedHello();
//after a second, you'll see it say "Hello"
A more common scenario is that you'd want to throttle function calls, say inside a scroll listener that's called many times per second. You only want to run your function once every half-second, no matter how many times it's called. That's an easy functional order:
var throttle = function(f, delay) {
var snooze = false;
return function() {
if (snooze) {
//we're sleeping, do nothing
return;
}
//otherwise, run our function and set snooze temporarily
f();
snooze = true;
setTimout(function() {
//after a delay, wake back up
snooze = false;
}, delay);
}
}
In both of these cases, however, we've called our function directly. Functions called this way, you may remember, have a this value of either the window object, or undefined. The same thing happens when we use setTimeout--because the function value is passed in, but it's not called in the context of its parent object, our desired value of this won't be preserved. However, we can force JavaScript to use a given this value by running functions with call() and apply() instead of direct invocation.
var logThis = function() {
console.log(this);
}
logThis(); //either window or undefined
//by using call(), we can set the this value
logThis.call(document); //logs out the document object
logThis.call(logThis); //logs out itself
Both call and apply are methods that are attached to every JavaScript function. As their first argument, they take the context object that you'd like to be the value of this for the function. Their difference comes from what they expect next: call will pass its next arguments in as arguments to the function, while apply only accepts an array as its second argument, and will expand those into the called function's argument list.
var logArgs = function(a, b, c) {
console.log(this);
console.log(a, b, c);
};
logArgs.call(document, 1, 2, 3); //logs out the document, followed by 1, 2, 3
logArgs.apply(jQuery, [4, 5, 6]); //logs out jQuery, followed by 4, 5, 6
Although call is more straightforward (and a little faster), apply actually enables a couple of useful tricks. For one thing, inside each function, its arguments are available as an array-like object named (of course) arguments. By passing this directly into apply, you can bind a function to a new permanent context, but still allow it to take arguments like normal:
var bind = function(f, context) {
return function() {
f.apply(context, arguments);
}
};
var bound = bind(function(a, b, c) {
console.log(this);
console.log(a, b, c);
}, document);
bound(1, 2, 3);
//logs document for the value of this, not window
//but also still logs out 1, 2, 3 as normal
Bound functions can be very useful for event listeners or object methods. If called directly or through a timeout, those methods would no longer know about their parent objects unless they're bound to its context. This is so handy, the same browsers that support functional operations like map also support bind as a third method on all functions alongside call and apply.