You may not hear much about higher-order functions (HoFs), but there are plenty of examples in everyday JavaScript. Let's take a quick look at how they are used and some common patterns.
Callbacks & HoFs
Higher-order functions accept or return a function. While they can take many shapes, most of the time we are dealing with functions that accept callbacks. If you've ever passed a callback to a function, you've used this functional programming concept!
Events
Perhaps the most common HoFs are for events. It doesn't matter whether you use a library, a framework, or write in plain JavaScript; event handlers are almost always callbacks passed to a higher-order function like addEventListener
.
const myButton = document.getElementById('myButton');
myButton.addEventListener('click', (event) => {
console.log('This is a callback function!');
console.log('We passed this to addEventListener.');
console.log('You clicked', event.target);
});
// Or in JSX
const Button = () => (<button onClick={
() => console.log('I\'m a callback!')
}>Test</button>);
What's in a Parameter Name?
An Event
is passed as the first argument, no matter what you name it.
// Some people prefer "e" or "evt"
myButton.addEventListener('click', (e) => {
console.log('You clicked', e.target);
});
// The name is your choice. The value is always an Event
myButton.addEventListener('click', (orange) => {
console.log('You clicked', orange.target);
});
It's still there even if you don't give it a name!
myButton.addEventListener('click', function () {
// "arguments" is not available for arrow functions
// but the first argument is still an Event
// even if you don't use it.
console.log('You clicked', arguments[0].target);
});
But the callback function doesn't have to be anonymous. Once you know the arguments a callback receives – like an Event
as the first argument no matter what – you can feel more confident splitting up the code.
const myButton = document.getElementById('myButton');
const theButtonAction = (event) => {
console.log('This is still a callback function!');
console.log('We passed this to addEventListener.');
console.log('You clicked', event.target);
};
// This will pass an Event to myButtonAction
myButton.addEventListener('click', theButtonAction);
This shows us one of the more powerful parts of HoFs: callbacks can be reused rather than creating new ones each time.
const myButton = document.getElementById('myButton');
const yourButton = document.getElementById('yourButton');
// Re-use the event handler on two different buttons
myButton.addEventListener('click', theButtonAction);
yourButton.addEventListener('click', theButtonAction);
Where Everybody Knows Your Name
In fact, this is essential to another event-handling HoF: removeEventListener
. We need a reference to the original function if we want to remove it.
const myButton = document.getElementById('myButton');
const theButtonAction = (event) => {
console.log('This is a callback function!');
console.log('We passed this to addEventListener.');
console.log('You clicked', event.target);
// Stopping the event handler needs the original function
myButton.removeEventListener('click', theButtonAction);
};
myButton.addEventListener('click', theButtonAction);
Our earlier example has no reference to the callback, so we cannot use removeEventHandler
. That handler accepts clicks as long as the element exists. In this newest example, the event handler only runs once because we are able to remove it.
Timeouts and Intervals
Timeouts feature in a lot of coding examples because they are an easy way to cause asynchronous events or simulate more complex behavior. These are also higher-order functions that take a callback.
setTimeout(() => console.log('After ~100 ms!'), 100);
setInterval(() => console.log('Every ~100 ms'), 100);
If you are unfamiliar with it, I use the tilde (~) to indicate approximately 100 milliseconds because timers & intervals are not precise.
Array Operations
Many references to HoFs and Functional Programming in JavaScript talk about the Array prototype methods like .map()
and .reduce()
. These can be very useful for processing data.
// Double
[ 1, 2, 3 ].map((value) => value * 2);
// [ 2, 4, 6 ]
// Sum
[ 1, 2, 3 ].reduce((accumulator, value) => value + accumulator, 0);
// 6
// Double and Sum (with chaining!)
[ 1, 2, 3 ]
.map((value) => value * 2)
.reduce((accumulator, value) => value + accumulator, 0);
// 12
These are also great examples of places where we can name and re-use functions.
const double = (value) => value * 2;
const sum = (value1, value2) => value1 + value2;
[ 1, 2, 3 ].map(double);
// [ 2, 4, 6 ];
[ 1, 2, 3 ].reduce(sum, 0);
// 6;
// Double and Sum (with chaining!)
[ 1, 2, 3 ]
.map(double)
.reduce(sum, 0);
// 12
Parameter Pairings
Knowing the parameters passed to callbacks for these HoFs is important to getting the correct response. The sum
function won't work with .map()
because map doesn't provide two values.
React
Many of the React hooks are HoFs, like useCallback
, useEffect
, useMemo
, and useReducer
.
useEffect
allows you to write your own HoF as well, because the function you pass to useEffect
can return a function which is called for cleanup.
useEffect(() => {
// This function is passed to useEffect
doSomething();
// Our function returns a function, too!
return () => {
cleanupSomething();
}
});
Of course, these don't have to be anonymous, though dependencies and consuming other hooks can complicate things.
const someEffect = () => {
doSomething();
return cleanupSomething;
};
useEffect(someEffect);
Samples & Suggestions
Try looking for anonymous callbacks like these in code samples and articles:
// Anonymous timeout
setTimeout((result) => doSomething(result), 100);
// Anonymous promise chain
Promise.resolve(data)
.then((result) => doSomething(result));
// Anonymous event handler
foo.addEventListener((event) => handleEvent(event));
// Anonymous array operation
data.map((row) => reformatRow(row));
When you find them, ask yourself if you need that wrapper at all:
// Not passing any arguments, so it's fine
setTimeout(doSomething, 100);
// Safe. Promises only pass one argument.
Promise.resolve(data)
.then(doSomething);
// Safe. Event handlers receive one argument.
foo.addEventListener(handleEvent);
// Depends. Does reformatRow accepts other arguments?
data.map((row) => reformatRow(row));
Limitations
We can't eliminate a wrapper function in every case. Some HoFs like Array.prototype.map
provide multiple arguments to their callback – value, index, array
in this case – and some functions operate differently when they get more arguments. The classic example is parseInt
, which accepts a base as the second argument. If you pass it directly to .map
it will receive the index
as the second argument, producing unexpected outcomes.
['9', '10', '11', '12', '13'].map(parseInt);
// [ 9, NaN, 3, 5, 7 ]
['9', '10', '11', '12', '13'].map((val) => parseInt(val));
// [ 9, 10, 11, 12, 13 ]
It's important to know what arguments are passed to your functions, and what arguments they accept.
Feedback
Thanks for reading, and let me know what you thought. Do you have any tips or suggestions for developers learning to understand higher-order functions? Comment or post a link to other articles!
Top comments (0)