Tutorial

How To Use map(), filter(), and reduce() in JavaScript

DevelopmentJavaScript

Introduction

Functional programming in JavaScript benefits code readability, maintainability, and testability. One of tools from the functional programming mindset is programming in an array-processing style. This entails taking arrays as your fundamental data structure. Then, your program becomes a series of operations on elements in the array.

There are many contexts in which this is useful, such as mapping AJAX results to React components with map, removing extraneous data with filter, and using reduce. These functions, called “Array Extras”, are abstractions over for loops. There is nothing you can do with with these functions that you can’t achieve with for, and vice versa.

In this tutorial, you’ll develop a deeper understanding of functional programming in JavaScript by taking a look at filter, map, and reduce.

Prerequisites

To complete this tutorial, you will need the following:

Step 1 — Iterating with forEach

for loops are used to iterate over every item in an array. Usually, something is done to each item along the way.

One example would be capitalizing every string in an array.

const strings = ['arielle', 'are', 'you', 'there'];
const capitalizedStrings = [];

for (let i = 0; i < strings.length; i += 1) {
    const string = strings[i];
    capitalizedStrings.push(string.toUpperCase());
}

console.log(capitalizedStrings);

In this snippet, you start with an array of lowercase phrases called strings. Then, an empty array called capitalizedStrings is initialized. The capitalizedStrings array will store the capitalized strings.

Inside of the for loop, the next string on every iteration is capitalized and pushed to capitalizedStrings. At the end of the loop, capitalizedStrings contains the capitalized version of every word in strings.

The forEach function can be used to make this code more succinct. This is an array method that “automatically” loops through the list. In other words, it handles the details of initializing and incrementing a counter.

Instead of the above, where you manually index into strings, you can call forEach and receive the next string on every iteration. The updated version would look something like:

const strings = ['arielle', 'are', 'you', 'there'];
const capitalizedStrings = [];

strings.forEach(function (string) {
    capitalizedStrings.push(string.toUpperCase());
})

console.log(capitalizedStrings);

This is very close to the initial function. But it removes the need for the i counter, making your code more readable.

This also introduces a major pattern you’ll see time and time again. Namely: it’s best to use methods on Array.prototype that abstract away details like initializing and incrementing counters. That way, you can focus on the important logic. There are several other array methods that this article will discuss. Moving forward, you will be using encryption and decryption to fully demonstrate what these methods can do.

Step 2 — Understanding the Caesar Cipher and Encryption and Decryption

In the snippets below, you’ll use the array methods map, reduce, and filter to encrypt and decrypt strings.

It’s important to first understand what encryption is. If you send a normal message like 'this is my super secret message' to a friend and someone else gets their hands on it, they can read the message immediately despite not being the intended recipient. This is a bad thing if you’re sending sensitive information, like passwords, which someone might be listening for.

Encrypting a string means: “scrambling it to make it hard to read without unscrambling.” This way, even if someone is listening and they do intercept your message, it will remain unreadable until they unscramble it.

There are different methods of encryption and the Caesar cipher is one way to scramble a string like this. This cipher is available to use in your code. Create a constant variable called caesar. To encrypt strings in your code, use the function below:

var caesarShift = function (str, amount) {
  if (amount < 0) {
    return caesarShift(str, amount + 26);
  }

  var output = "";

  for (var i = 0; i < str.length; i++) {
    var c = str[i];

    if (c.match(/[a-z]/i)) {
      var code = str.charCodeAt(i);

      if (code >= 65 && code <= 90) {
        c = String.fromCharCode(((code - 65 + amount) % 26) + 65);
      }

      else if (code >= 97 && code <= 122) {
        c = String.fromCharCode(((code - 97 + amount) % 26) + 97);
      }
    }

    output += c;
  }

  return output;
};

This GitHub gist contains the original code for this Caesar cipher function created by Evan Hahn.

To encrypt with the Caesar cipher, you have to pick a key n, between 1 and 25, and replace every letter in the original string with the one n letters further down the alphabet. So, if you choose the key 2, a becomes c; b becomes d; c becomes e; etc.

Substituting letters like this makes the original string unreadable. Since the strings are scrambled by shifting letters, they can be unscrambled by shifting them back. If you get a message you know was encrypted with the key 2, all you need to do to decrypt is shift letters back two spaces. So, c becomes a; d becomes b; etc. To see the Caesar cipher in action, call the caesarShift function and pass in the string 'this is my super secret message.' as the first argument and the number 2 as the second argument:

const encryptedString = caesarShift('this is my super secret message.', 2);

In this code, the message is scrambled by shifting each letter forward by 2 letters: a becomes c; s becomes u; etc. To see the results, use console.log to print encryptedString to the console:

const encryptedString = caesarShift('this is my super secret message.', 2);

console.log(encryptedString);

To quote the example above, the message 'this is my super secret message' becomes the scrambled message 'vjku ku oa uwrgt ugetgv uvtkpi.'.

Unfortunately, this form of encryption is easy to break. One way to decrypt any string encrypted with a Caesar cipher is to just try to decrypt it with every possible key. One of the results will be correct.

For some of the code examples to come, you will need to decrypt some encrypted messages. This tryAll function can be used to do that:

const tryAll = function (encryptedString) {
    const decryptionAttempts = []

    while (decryptionAttempts.length < 26) {
        const decryptionKey = -decryptionAttempts.length;
        const decryptionAttempt = caesarShift(encryptedString, decryptionKey);

        decryptionAttempts.push(decryptionAttempt)
    }

    return decryptionAttempts;
};

The above function takes an encrypted string, and returns an array of every possible decryption. One of those results will be the string you want. So, this always breaks the cipher.

It is challenging to scan an array of 26 possible decryptions. It’s possible to eliminate ones that are definitely incorrect. You can use this function isEnglish to do this:

'use strict'
const fs = require('fs')

const _getFrequencyList = () => {
    const frequencyList = fs.readFileSync(`${__dirname}/eng_10k.txt`).toString().split('\n').slice(1000)
    const dict = {};

    frequencyList.forEach(word => {
        if (!word.match(/[aeuoi]/gi)) {
            return;
        }

        dict[word] = word;
    })

    return dict;
}

const isEnglish = string => {
    const threshold = 3;

    if (string.split(/\s/).length < 6) {
        return true;
    } else {
        let count = 0;
        const frequencyList = _getFrequencyList();

        string.split(/\s/).forEach(function (string) {
            const adjusted = string.toLowerCase().replace(/\./g, '')

            if (frequencyList[adjusted]) {
                count += 1;
            }
        })

        return count > threshold;
    }
}

This GitHub gist contains the original code for both tryAll and isEnglish created by Peleke Sengstacke.

Make sure to save this list of the most common 1,000 words in English as eng_10k.txt.

You can include all of these functions in the same JavaScript file or you can import each function as a module.

The isEnglish function reads a string, counts how many of the most common 1,000 words in English occur in that string and, if it finds more than 3 of those words in the sentence, it classifies the string as English. If the string contains fewer than 3 words from that array, it throws it out.

In the section on filter, you’ll use the isEnglish function.

You will use these functions to demonstrate how array methods map, filter, and reduce work. The map method will be covered in the next step.

Step 3 — Transforming Arrays with map

Refactoring a for loop to use forEach hints at advantages of this style. But there’s still room for improvement. In the previous example, the capitalizedStrings array is being updated within the callback to forEach. There’s nothing inherently wrong with this. But it’s best to avoid side effects like this whenever possible. If a data structure that lives in a different scope doesn’t have to be updated, it’s best to avoid doing this.

In this particular case, you wanted to turn every string in strings into its capitalized version. This is a very common use case for a for loop: take everything in an array, turn it into something else, and collect the results in a new array.

Transforming every element in an array into a new one and collecting the results is called mapping. JavaScript has a built-in function for this use case, called, map. The forEach method is used because it abstracts away the need to manage the iteration variable, i. This means you can focus on the logic that really matters. Similarly, map is used because it abstracts away initializing an empty array, and pushing to it. Just like forEach accepts a callback that does something with each string value, map accepts a callback that does something with each string value.

Let’s look at a quick demo before a final explanation. In the following example, the encryption functions will be used. You could use a for loop or forEach. But it’s best to use map in this instance.

To demonstrate how to use the map function, create 2 constant variables: one called key that has a value of 12 and an array called messages:

const key = 12;

const messages = [
    'arielle, are you there?',
    'the ghost has killed the shell',
    'the liziorati attack at dawn'
]

Now create a constant called encryptedMessages. Use the map function on messages:

const encryptedMessages = messages.map()

Within map, create a function that has a parameter string:

const encryptedMessages = messages.map(function (string) {

})

Inside of this function, create a return statement which will return the cipher function caesarShift. The caesarShift function should have string and key as its arguments:

const encryptedMessages = messages.map(function (string) {
    return caesarShift(string, key);
})

Print encryptedMessages to the console to see the results:

const encryptedMessages = messages.map(function (string) {
    return caesarShift(string, key);
})

console.log(encryptedMessages);

Note what happened here. The map method was used on messages to encrypt each string with the caesar function, and automatically store the result in a new array.

After the above code runs, encryptedMessages looks like: ['mduqxxq, mdq kag ftqdq?', 'ftq staef tme wuxxqp ftq etqxx', 'ftq xuluadmfu mffmow mf pmiz']. This is a much higher level of abstraction than manually pushing to an array.

You can refactor encryptedMessages using arrow functions to make your code more concise:

const encryptedMessages = messages.map(string => caesarShift(string, key));

Now that you have a thorough understanding of how map works, you can use the filter array method.

Step 4 — Throwing Things Away with filter

Another common pattern is using a for loop to process items in an array, but only pushing/preserving some of array items. Normally, if statements are used to decide which items to keep and which to throw away.

In raw JavaScript, this might look like:

const encryptedMessage = 'mduqxxq, mdq kag ftqdq?';

const possibilities = tryAll(encryptedMessage);

const likelyPossibilities = [];

possibilities.forEach(function (decryptionAttempt) {
    if (isEnglish(decryptionAttempt)) {
        likelyPossibilities.push(decryptionAttempt);
    }
})

The tryAll function is use to decrypt encryptedMessage. That means you’ll end up with an array of 26 possibilities.

Since most of the decryption attempts won’t be readable, a forEach loop is used to check if each decrypted string is English with the isEnglish function. The strings that are in English are pushed to an array called likelyPossibilities.

This is a common use case. So, there’s a built-in for it called filter. As with map, filter is given a callback, which also gets each string. The difference is that, filter will only save items in an array if the callback returns true.

You can refactor the above code snippet to use filter instead of forEach. The likelyPossibilities variable will no longer be an empty array. Instead set it equal to the possibilities array. Call the filter method on possibilities:

const likelyPossibilities = possibilities.filter()

Within filter, create a function that takes a parameter called string:

const likelyPossibilities = possibilities.filter(function (string) {

})

Within this function, use a return statement to return the results of isEnglish with string passed in as its argument:

const likelyPossibilities = possibilities.filter(function (string) {
    return isEnglish(string);
})

If isEnglish(string) returns true, filter saves string in the new likelyPossibilities array.

Since this callback calls isEnglish, this code can be refactored further to be more concise:

const likelyPossibilities = possibilities.filter(isEnglish);

The reduce method is another abstraction that is very important to know.

Step 5 — Bringing Things Together with reduce

Iterating over an array to collect its elements into a single result is a very common use case.

A good example of this is using a for loop to iterate through an array of numbers and add all the numbers together:

const prices = [12, 19, 7, 209];

let totalPrice = 0;

for (let i = 0; i < prices.length; i += 1) {
    totalPrice += prices[i];
}

console.log(`Your total is ${totalPrice}.`);

The numbers in prices are looped through and each number is added to totalPrice. The reduce method is an abstraction for this use case.

You can refactor the above loop with reduce. You won’t need the totalPrice variable anymore. Call the reduce method on prices:

const prices = [12, 19, 7, 209];

prices.reduce()

The reduce method will hold a callback function. Unlike map and filter, the callback passed to reduce accepts two arguments: the total accumulative price and the next price in the array to be added to the total. This will be totalPrice and nextPrice respectively:

prices.reduce(function (totalPrice, nextPrice) {

})

To break this down further, totalPrice is like total in the first example. It’s the total price after adding up all the prices received seen so far.

In comparison to the previous example, nextPrice corresponds to prices[i]. Recall that map and reduce automatically index into the array, and pass this value to their callbacks automatically. The reduce method does the same thing, but passes that value as the second argument to its callback.

Include two console.log statements within the function that print totalPrice and nextPrice to the console:

prices.reduce(function (totalPrice, nextPrice) {
    console.log(`Total price so far: ${totalPrice}`)
    console.log(`Next price to add: ${nextPrice}`)
})

You’ll need to update totalPrice to include each new nextPrice:

prices.reduce(function (totalPrice, nextPrice) {
    console.log(`Total price so far: ${totalPrice}`)
    console.log(`Next price to add: ${nextPrice}`)

    totalPrice += nextPrice
})

Just like with map and reduce, on each iteration, a value needs to be returned. In this case, that value is totalPrice. So create a return statement for totalPrice:

prices.reduce(function (totalPrice, nextPrice) {
    console.log(`Total price so far: ${totalPrice}`)
    console.log(`Next price to add: ${nextPrice}`)

    totalPrice += nextPrice

    return totalPrice
})

The reduce method takes two arguments. The first is the callback function which has already been created. The second argument is a number which will act as the starting value for totalPrice. This corresponds to const total = 0 in the previous example.

prices.reduce(function (totalPrice, nextPrice) {
    console.log(`Total price so far: ${totalPrice}`)
    console.log(`Next price to add: ${nextPrice}`)

    totalPrice += nextPrice

    return totalPrice
}, 0)

As you’ve now seen, reduce can be used to collect an array of numbers into a sum. But reduce is versatile. It can be used to turn an array into any single result, not just numeric values.

For example, reduce can be used to build up a string. To see this in action, first create an array of strings. The example below uses an array of computer science courses called courses:

const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];

Create a constant variable called curriculum. Call the reduce method on courses. The callback function should have two arguments: courseList and course:

const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];

const curriculum = courses.reduce(function (courseList, course) {

});

courseList will need to be updated to include each new course:

const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];

const curriculum = courses.reduce(function (courseList, course) {
    return courseList += `\n\t${course}`;
});

The \n\t will create a newline and tab for indentation before each course.

The first argument for reduce (a callback function) is completed. Because a string is being constructed, not a number, the second argument will also be a string.

The example below uses 'The Computer Science curriculum consists of:' as the second argument for reduce. Add a console.log statement to print curriculum to the console:

const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];

const curriculum = courses.reduce(function (courseList, course) {
    return courseList += `\n\t${course}`;
}, 'The Computer Science curriculum consists of:');

console.log(curriculum);

This generates the output:

Output
The Computer Science curriculum consists of: Introduction to Programming Algorithms & Data Structures Discrete Math

As stated earlier, reduce is versatile. It can be used to turn an array into any kind of single result. That single result can even be an array.

Create an array of strings:

const names = ['arielle', 'jung', 'scheherazade'];

The titleCase function will capitalize the first letter in a string:

const names = ['arielle', 'jung', 'scheherazade'];

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

titleCase capitalizes the first letter in a string by grabbing the first letter of a string at the 0 index, using toUpperCase on that letter, grabbing the rest of the string, and joining everything together.

With titleCase in place, create a constant variable called titleCased. Set it equal to names and call the reduce method on names:

const names = ['arielle', 'jung', 'scheherazade'];

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce()

The reduce method will have a callback function that takes titleCasedNames and name as arguments:

const names = ['arielle', 'jung', 'scheherazade'];

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {

})

Within the callback function, create a constant variable called titleCasedName. Call the titleCase function and pass in name as the argument:

const names = ['arielle', 'jung', 'scheherazade'];

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {
    const titleCasedName = titleCase(name);
})

This will capitalize the each name in names. The first callback argument of the callback function titleCasedNames will be an array. Push titleCasedName (the capitalized version of name) to this array and return titleCaseNames:

const names = ['arielle', 'jung', 'scheherazade'];

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {
    const titleCasedName = titleCase(name);

    titleCasedNames.push(titleCasedName);

    return titleCasedNames;
})

The reduce method requires two arguments. The first which is the callback function is complete. Since this method is creating a new array, the initial value will be an empty array. Also include console.log to print the final results to the screen:

const names = ['arielle', 'jung', 'scheherazade'];

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {
    const titleCasedName = titleCase(name);

    titleCasedNames.push(titleCasedName);

    return titleCasedNames;
}, [])

console.log(titleCased);

After running your code, it will produce the following array of capitalized names:

Output
["Arielle", "Jung", "Scheherazade"]

You used reduce to turn an array of lower-cased names into an array of title-cased names.

The previous examples prove that reduce can be used to turn a list of numbers into a single sum and can be used to turn a list of strings into a single string. Here, you used reduce to turn an array of lower-cased names into a single array of upper-cased names. This is still valid use case because the single list of upper-cased names is still a single result. It just happens to be a collection, rather than a primitive type.

Conclusion

In this tutorial, you’ve learned how to use map, filter, and reduce to write more readable code. There isn’t anything wrong with using the for loop. But raising the level of abstraction with these functions brings immediate benefits to readability and maintainability, by corollary.

From here, you can begin to explore other array methods such as flatten and flatMap. This article called Flatten Arrays in Vanilla JavaScript with flat() and flatMap() is a great starting point.

Creative Commons License