Unless today is your first time in the JavaScript world, you should have already felt the trending buzz around the higher-order functions map, reduce and filter. These powerful functions allow us to perform complex operations in a blink.
Why we label them ‘higher-order functions’, how we abstract them, and how to build them are the focus of this article In addition, we will quickly gloss over these essential concepts: callbacks, pure functions, and first-class functions. Lace your boots and let’s go!
Prerequisites
To follow and understand this article, you should have some basic experience in using the javascript language. You must be familiar with loops and functions, including how to use map, reduce and filter.
Table of Contents
- Why higher-order?
- Pure functions
- Let’s Code
- Map
- Filter
- Reduce
Why higher-order?
Let’s look at an example, one in which we square the numbers in a given array. We can do this in two ways using map.
- Define a squareNumber function separately and pass it into the map function.
const nums = [ 1, 2, 3, 4, 5 ]; function squareNumber(num) { return num * num; } let result= nums.map(squareNumber); console.log(result); // [ 1, 4, 9, 16, 25 ]
- Pass the squareNumber function directly into the map function.
const nums = [ 1, 2, 3, 4, 5 ] Let result = nums.map(num => num * num) console.log(result); // [ 1, 4, 9, 16, 25 ]
Notice that we had to pass a function as an argument into the map each time, and that is true for every use case of map. The function passed is then used internally to act on each item in the given array. In our case, the squareNumber function is called sequentially on each of the numbers, which we can’t see because it is abstracted in the original definition of the map function.
Using our example, here is a basic overview of what happens internally.
squareNumber(1) // 1 squareNumber(2) // 4 squareNumber(3) // 9 squareNumber(4) // 16 squareNumber(5) // 25
The result, 1 4 9 16 25, is packed into a new array and returned to us.
This property, taking functions as arguments and acting on them, is why map is called a higher-order function. A clearer definition can be sampled from the Eloquent Javascript book, “Functions that operate on other functions, either by taking them as arguments or by returning them, are called higher–order functions.”
Moreover, the function that is passed as the argument is called a callback.
All these terms tied together result in the concept of first-class functions. To borrow MDN’s definition:
A programming language is said to have First-class functions when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable.
Pure Functions
Two important things happen every time we use map to generate the array of squared numbers:
- The input array remains unchanged
- It creates a new array that holds the results
If we were to modify the input array, it will result in unintended consequences in other parts of the program. What if the user wants to re-use that same input array in other parts of the program for some other purpose? For that reason, the input array is maintained such that anytime it is passed in the map function, the same output is generated.
Imagine that for every instance we use the map function, the original input is modified to hold the result obtained? In that case, if nums is initially [1, 2, 3, 4, 5 ], at the end of the operation, nums now becomes [1, 4, 9, 16, 25]. If we proceed to perform another map operation, nums becomes [1, 16, 81, 256, 625]. At the end of the day, we will lose track of the original content of nums, which may be detrimental to the operation of our program.
To preserve the original input array, a new array array is created and filled with the results obtained. That way, whenever we intend to use nums in other parts of our program, we know it contains [1, 2, 3, 4, 5].
Functions that return the same output for the same input are called Pure Functions. They don’t rely on or change variables outside their scope. There is more to the concept of pure functions but what we have covered so far is enough for what we are going to do next. Read more on pure functions here.
Let’s Code
Based on what we have covered, we know:
- Our higher order functions must take a callback function as an argument (which is called on each element in the input array).
- They must return a new array filled with the results.
Therefore, the skeleton code for all of our higher order functions would like this:
function higherOrder(callback){ let output = [] // callback operations go here return output } // using the function let result = higherOrder(callback)
But we are missing one crucial thing: the input array. Remember how we called the map function? nums.map(squareNumber). Where is the input array (nums) in the above defined skeleton?
In JavaScript, higher order functions are defined in the prototype of the Array Object. This definition makes it so that the functions are called as methods on the array like so array.map…, array.filter…, and array.reduce…
Defining our custom functions in the Array prototype would override the already existing ones in there. We will rather pass the input array as an argument to our custom function. Hence, instead of nums.map(squareNumber), we do map(nums,squareNumber).
Our final code skeleton would then look like this:
function higherOrder(input, callback){ let output = [] // other operations go here return output } // calling the function let result = higherOrder(input,callback)
We can finally code our custom higher-order functions.
Map
Remember that the callback function is called on each element in the input array. The result of each call is stored in a new output array and returned.
In the below code, we use a for-loop to run through the elements in the input array, calling the callback on each in the process. We then push the result in each iteration to the output array.
So, if we pass squareNumber (scroll up to its definition) as the callback, squareNumber will be called on each number in the array and the result is stored in the output.
function map(input, callback){ let output = [] for(let i = 0; i < input.length; i++){ result = callback(input[i]); output.push(result) } return output; }
If we pass squareNumber (scroll up to its definition) as the call back, this is what happens in the for-loop:
result = squareNumber(1) //1 output.push(1) // output = [1] result = squareNumber(2) // 4 output.push(4) // output = [1, 4] result = squareNumber(3) // 9 output.push(9) // output = [1, 4, 9] result = squareNumber(4) // 16 output.push(16) // output = [1, 4, 9, 16] result = squareNumber(5) // 25 output.push(25) // output = [1, 4, 9, 16, 25]
Filter
When given an array of numbers say, [1, 2, 3, 4, 5] and we wish to return all numbers greater than 2, we will use filter like so:
const nums = [1, 2, 3, 4, 5] function isGreaterThan2(num){ return num > 2 } let result = nums.filter(isGreaterThan2); console.log(result) // [3, 4, 5]
Or
const nums = [1, 2, 3, 4, 5] let result = nums.filter(num => num > 2); console.log(result) // [3, 4, 5]
Here is what happens internally:
isGreaterThan2(1) // false isGreaterThan2(2) // false isGreaterThan2(3) // true isGreaterThan2(4) // true isGreaterThan2(5) // true
Then eventually, the numbers that returned true are packed into a new array and returned.
Hence, in coding our custom filter function, there must be an additional if-statement in each iteration of the input array to check for the numbers that satisfy the given condition (of being greater than 2). Those numbers will be the ones we push into the output array.
Armed with this knowledge, could you try coding the filter function on your own before checking the solution below?
You got this!
function filter(input, callback){ let output = [] for(let i = 0; i < input.length; i++){ if(callback(input[i]){ output.push(input[i]) } } return output }
Reduce
Reduce is quite tricky. Unlike map and filer, reduce returns a single value. It essentially ‘reduces’ the input array into a single output. Let’s look at an example which sums up all the numbers in an array.
const nums = [1, 2, 3, 4, 5] function sum(total, num){ return total + sum } let result = nums.reduce(sum,0) // Or let result = nums.reduce(sum); console.log(result) // 15
In using reduce, there is a second optional argument. This argument acts as the initial value. In our example, since we wanted to sum all the numbers, it made sense for us to pass zero as the initial value. If we do not pass a value for the initial value, we use the first item in the input array.
Unlike map and filter where we had an output array to store the results, we don’t need one for reduce. Instead we need an accumulator variable (not an array) to hold the new results on successive operations.
Also, in our custom reduce function, we need to cater for when no initial value is passed. In that case, we can set the accumulator value as the first value in the input array. Then skip the first value in the array when looping through to apply the callback.
Here we go:
function reduce(input, callback, initialValue){ let accumulator = initialValue; let i = 0; // when initialValue is not passed if(accumulator == null || accumulator == undefined){ accumulator = input[i]; i++ // skipping first index in input array } // start looping from i (in case we skipped first index) for(let j = i; j < input.length; j++){ accumulator = callback(accumulator,input[j]) } return accumulator; }
To spice things up, let’s throw an error when the input array is empty.
function reduce(input, callback, initialValue){ if(array.length == 0) throw new Error("array is empty") let accumulator = initialValue; let i = 0; if(accumulator == null || accumulator == undefined){ accumulator = input[i]; i++ } for(let j = i; j < input.length; j++){ accumulator = callback(accumulator,input[i]) } return accumulator }
Conclusion
By now you should be able to code the other higher order functions associated with JavaScript arrays. Challenge yourself!
Looking to understand more about what you can do in JavaScript, take a look at this article, “Understanding the JavaScript DOM Manipulations.”