What to do with lists of things in JavaScript
Klemen Slavič
Posted on September 23, 2018
Cover image by Internet Archive Book Image
In JavaScript, as in most languages, we have a data structure that deals with lists of values. It's a very handy object that lets us group values together in an ordered list. But there's much more to arrays in JavaScript than just the string index and length
property.
JavaScript has borrowed some of the functions that functional programming languages implement in their standard libraries, and made them a bit more convenient by binding them to the Array
prototype. In a follow-up post, we'll see how we can apply functional approaches to writing programs that compose better than standard procedural code.
But first, let's dive into the basics.
Part 1: Search
There are many ways to skin this particular cat, depending on what you want to achieve. Let's take a fun data source that provides a list of things that we can practice our JS-fu on:
// we set up the data fetch and hand the data
// to our main function
const fetch = require('node-fetch');
const SOURCE_URL = 'https://www.reddit.com/r/reactiongifs.json';
fetch(SOURCE_URL)
.then(response => response.json())
.then(main)
.catch(err => console.error(err));
// ----[main]----
function main(json) {
// here's where we deal with the data
console.log(json.data.children);
}
We'll be using /r/reactiongifs
on Reddit. Run the example above to see what we're dealing with.
Hint: Any Reddit page can be fetched in JSON form by appending the .json
suffix to the URL. Try it!
Question: does every list item match a particular criteria?
Say that we wanted to check that every post in the list contains the acronym MRW
in the title
field. For this, we use the every()
function on the list.
const postTitleContainsMRW = post => post.data.title.includes('MRW');
function main(json) {
const posts = json.data.children;
const eachContainsMRW = posts.every(postTitleContainsMRW);
console.log('Every post contains MRW?', eachContainsMRW);
}
Note: When the function supplied to every()
returns false
, it stops iterating over the array and immediately returns false
. If all items in the array resolve to true
, it returns true
.
Question: does the list contain any items matching a criteria?
OK, what about if we just want to check if any value matches? Let's look for the word cat
in the title using some()
.
const postTitleContainsCat = post => post.data.title.includes('cat');
function main(json) {
const posts = json.data.children;
const anyContainsCat = posts.some(postTitleContainsCat);
console.log('Does any post contain the word cat?', anyContainsCat);
}
Note: Since this function is the complement of every()
, it will stop iteration as soon as the first item resolves to true
. If none of the items resolve to true
, it returns false
.
Question: what's the first item in the list that matches a criteria?
Assuming the answer above was correct (it is dynamic data, after all!), let's find the first post that had the word cat
in it. For this, we can use find()
.
const postTitleContainsCat = post => post.data.title.includes('cat');
function main(json) {
const posts = json.data.children;
const catPost = posts.find(postTitleContainsCat);
console.log(catPost);
}
If no element is found, it returns undefined
.
Question: which position is the first found item in?
Just substitute find()
by findIndex()
and hey presto!
const postTitleContainsCat = post => post.data.title.includes('cat')
function main(json) {
const posts = json.data.children;
const catPostIndex = posts.findIndex(postTitleContainsCat);
console.log(catPostIndex);
}
Part 2: Transformation
So far, the methods described above only scan the contents, but other more useful methods allow us to transform an array into something else. Let's start with the basics, though.
Task: get a list of posts matching a criteria
Previously, we only cared about a single (first) value in the array. What about the rest? filter()
allows you to do just that.
const postTitleContainsCat = post => post.data.title.includes('cat');
function main(json) {
const posts = json.data.children;
const postsWithCats = posts.filter(postTitleContainsCat);
console.log(postsWithCats);
}
Task: convert each item in the array
Sometimes we need to take an object and transform it into a different format to be consumed by some other component or function. In this case, we can use the map()
function.
const simplifyPost = post => ({
title: post.data.title,
image: post.data.thumbnail,
animation: post.data.url
});
function main(json) {
const posts = json.data.children;
const simplerPosts = posts.map(simplifyPost);
console.log(simplerPosts);
}
Note: map()
returns a new array of items without changing the original array.
Task: create a summary of the list of items
If you need to produce any kind of summation, rollup or transformation on a list of items, reduce()
is the way to go. The gist of this operation is that you give it an initial value, and the function supplied to it will return the next value after processing each item in turn.
For this example, let's create a Set
of all words used in the title. Set
s are quite useful as they take care of deduplication of items that are already in the set.
const addWordsFromTitle = (set, post) => {
// we lowercase the title first
const title = post.data.title.toLowerCase();
// we split along every word boundary which isn't an apostrophe
const words = title.split(/[^\w']+/);
// for each non-empty word, we add it to the set
words.filter(word => word.length > 0)
.forEach(word => set.add(word));
// IMPORTANT: we return the set as the next value
return set;
};
function main(json) {
const posts = json.data.children;
// NOTE: here we start with an empty set and add words to it
const allWords = posts.reduce(addWordsFromTitle, new Set());
console.log(allWords);
}
This is a very powerful transformational method and can express almost any kind of operation you can think of, including all of the ones described above! If you want a quick taste of things you can do with just reduce
(or fold
, as it's called in functional languages), have a look at Brian Lonsdorf's talk below:
Task: order the items within a list
If we want to sort arbitrary values, we need to provide a comparator so that we can tell the sorting algorithm about ordering. To do this, we need to provide a function that takes two items from the array and returns one of three values:
-
-1
: when the first item should be before the second item (any negative number will do) -
0
: when the two items are equivalent in order -
1
: when the second item should come before the first item (any positive number will do)
Let's sort the items based on title length in decreasing order (longest first). If two titles have the same length, order them alphabetically.
const comparePosts = (a, b) => {
const titleA = a.data.title.toLowerCase();
const titleB = b.data.title.toLowerCase();
if (titleA.length > titleB.length) return -1;
if (titleA.length < titleB.length) return 1;
return titleA.localeCompare(titleB, 'en', { sensitivity: 'base' });
};
function main(json) {
// Array.from() creates a copy of the array so that we don't
// modify the original data
const posts = Array.from(json.data.children);
posts.sort(comparePosts);
console.log(posts);
}
Note: sort()
sorts the array in-place, which means that the original array is modified.
Wrapping up
This post covers just the basics of array methods that we'll need when we start implementing a more functional approach in our examples. Until then, keep in mind that whenever you feel the need to write a for
loop over an Array
, there's probably a way to write that same thing using the methods described above.
Stay curious!
Posted on September 23, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.