Functional programming in JavaScript benefits code readability, maintainability, and testability. One of the 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 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
.
To complete this tutorial, you will need the following:
for
loops in JavaScript. This article on for
loops in JavaScript is a great place to start.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 this:
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.
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 make 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 oguucig.'
.
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;
}
}
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.
map
to Transform ArraysRefactoring a for
loop to use forEach
hints at the 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.
filter
to Select Values from an ArrayAnother 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.
reduce
to Turn an Array into a Single ValueIterating 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 that 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:
OutputThe 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 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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!