Joel Hassan

JavsScript higher-order functions - map, filter & reduce

2019-06-16

Higher order functions


Introduction

If you're like me and are accustomed to OOP, a lot of the functional programming concepts so common in JavaScript can take some time getting used to. One of these is the behaviour of functions, which behave differently to what you'd expect if coming from a language such as Java.

The Role of Functions JavaScript

In JavaScript, functions are objects - they too have properties and methods just like regular objects, and for this reason are known as '1st-class objects' - i.e. they have all the rights of regular objects in the sense that whatever can be done to objects can be done to functions. This means that we can, for example:

  • assign them to variables
  • pass them as arguments to other functions
  • return them from functions

Another thing to note is that anything that's not a primitive data type (such as a number or string) is an object in JavaScript.

Since functions can be used like all other objects, we can create anonymous functions and assign them to variables.

e.g.

// storing a function in a variable
let functInVariable = function(someValue){
    return someValue + 1; 
}

functInVariable(3); // output: 4

// passing the function object around
let newFunctVariable = functInVariable;

Because of the versatile nature of objects (which includes functions) in JS, we can even pass them as arguments to other functions and have functions return them too. This brings us into the world of higher-order functions.


Higher-order Functions

It's a fancy name, but all it refers to is a function that operates on other functions, either by:

  1. taking a function as an argument OR
  2. returning a function.

The concept is from mathematics and the definition here is the same. Here's what Wikipedia says:

In mathematics and computer science, a higher-order function is a function that does at least one of the following:

  • takes one or more functions as arguments (i.e. procedural parameters) OR
  • returns a function as its result.

&

All other functions are first-order functions.

How are they useful?

They're essential, because of the event-driven nature of JavaScript. This means that if a given function has some code that can't be executed immediately (such as an API call or a timeout), JavaScript doesn't stop and wait for a response to carry execution. Instead, it continues to listen for other events.

An example of something really common would be functions that receive callback functions as arguments.

Here's a good example from SitePoint:

function first(){
  // Simulate a code delay
  setTimeout( function(){
    console.log(1);
  }, 500 );
}

function second(){
  console.log(2);
}

first();
second();
// Output:
// 2
// 1

Although first() was called first, the result of second() was logged before it. That was because there was a delay in calling console.log(1), which shows how JavaScript carries on executing without waiting for a response from a function that's taking time to produce a response.

Composition

The idea is that we can create small functions, each taking care of a particular task, and compose more useful functions from them. This is what makes higher-order functions powerful.

Now that we've got a brief overview of functions in JavaScript, we can have a look at three very common higher-order functions: map, reduce & filter:


💡 Note: all arrays inherit from something called an Array.prototype, which specifies how arrays behave by default in JavaScript. By taking a look at the API, you can see all the various properties and methods attached to standard arrays.

Filter()

It does what it says on the tin - it filters out (rids) the array of elements. You'd use it when you have an array, but want to create a new one that has elements that fulfill certain condition(s).

It works like this: you have your existing array, you define a filter for each element to go through, and those that pass the test make it to the new array that the method returns.

More formally, the Array.prototype.filter():

creates a new array with all of the elements of this array for which the provided filtering function returns true.

The filtering function is a callback that the filter method takes as an argument. If it returns true for a given element in the original array, it is copied onto the newly-created shorter array.

Let's take a look at an example:

// an array with four 'person' objects

const persons = [
  
    // each has 2 properties: name and age
  
    { name : 'person1', age: 10},
    { name : 'person2', age: 32},
    { name : 'person3', age: 16},
    { name : 'person4', age: 30}
];

// using a regular function

const adults = persons.filter(function(person) { 
    // only persons 2,3 & 4 will be pushed to the new array
    return person.age >= 18;
});

// using an array function (ES6) - more compact 

const adults = persons.filter(person => person.age >= 18);

Now our new adults array only contains adults, the others were 'filtered out'. How did this happen?

The filter() method takes one argument - another function. This function is a callback that we have defined here as:

function(person) { 
    return person.age >= 18;
}

The filter() method iterates through all the array elements (person objects), passing each one as an argument to this callback.

The callback, in turn, returns a boolean true/false based on the person.age >= 18 evaluation. If true, the array element (in this case a person object) is added to the new array. What happens to the elements the expression evaluates as false? They're thrown away. The array returned by the filter() method only contains objects from the source array that 'passed through' the filter.

Similarly, in persons.filter(person => person.age >= 18) the filter method looks at each Array elements 'age' property, checks if its equal/greater than 18, and pushes it to the new 'adults' array if true.

The same could have been achieved using a for-loop:

const adults = [] // empty array for the adults

for (let loopIndex = 0; loopIndex < persons.length; loopIndex++) {
    if(persons[loopIndex]. age >= 18)
        adults.push(persons[loopIndex])
}

Which is less clear and less elegant. Also, the push method mutates the adults array it's called on. We don't want to do this as side-effects should be avoided in functional programming.


Map()

According to the MDN docs the Array.prototype.map():

creates a new array with the results of calling a provided function on every element in this array.

It's very similar to filter(), however the map() method does not throw the array elements away. Instead, it populates the new array based on the type of callback function. It will always return an array of the same number of elements as the original array whose elements we're mapping. As such, you can think of it as transforming the elements into some other form.

Continuing with the above example:

// gathering age figurs - a sub-set
const allAges = persons.map(person => person.age);
console.log(allAges); // [ 10, 32, 16, 30 ]

// doing the same using a regular function
const allAges = persons.map(person => {
    return person.age;
});

The callback function above simply returns the age property for each array element (object). A new array is then (again) constructed from the results.

The filter() method only added values to the new array for which the callback returned a true. The map() method, on the other hand, expects the callback function to return any type of object.

Using this, we could do this:

const howOld = persons.map(
  person => person.name + ' is ' + person.age + ' years old.'
);
console.log(howOld);

/* Output:

[ 'person1 is 10 years old.',
  'person2 is 32 years old.',
  'person3 is 16 years old.',
  'person4 is 30 years old.' ] */
  

We mapped each person to a string containing their name and age. Pretty neat 👍

The map() method is useful in cases where we want a subset of list's elements or to fill a new array consisting of the elements manipulated in some way.


Reduce()

This one is probably the most difficult of the three to grasp, it was for me at least. It's also the most flexible and powerful.

MDN - Array.prototype.reduce():

apply a function against an accumulator and each value of the array (from left-to-right) as to reduce it to a single value.

In simple terms, something (based on our reducer callback) is done to the array element currently being iterated and the result of that is passed to the next element (the next iteration).

Ultimately, the method reduces (or combines) the whole array into a single value, such as a sum, average, concatenated string or whatever we want it to be.

Back to the example, say we wanted to calculate the total age of all the persons in our array. We'd do it as so:

const combinedAge = persons.reduce((totalAge, person) => {
  return totalAge + person.age;
  // sum is initially 0
}, 0);

console.log(combinedAge); // 88

The initial total age is 0 - we set it so by passing 0 as the second argument to the reduce() method, in addition to our callback.

The reduce() method is extremely flexible and is typically used when the other array methods can't get the job done (or in their place).


Conclusion

Higher-order functions play a key part in JavaScript. They enable JavaScript to run asynchronously (by letting stuff happen in the background) and are often combined to achieve useful and reusable composition functions. There's a lot more of them and to them than this post covered, and there is great material on the topic. I'd personally recommend Fun Fun Function. His explanations are great! I hope you gained some clarity from this post 😊