8.

More Arrays

We’re not done with arrays yet! Here we introduce methods that allow us to deal with an entire array as a whole, no matter how big. This is invaluable for solving real-world problems. In order to do that, we need to use higher-order functions. Keep in mind that methods are just functions with slightly different syntax, so we could call them higher-order methods, but since this applies to functions in general, we’ll stick to the former.

A higher-order function is one that takes in a function as an argument. That’s right, functions can take other functions as arguments! And why shouldn’t they? After all, functions are values just like any other.

The important thing to note in this lesson is that we’re looking at patterns. Each of the following higher-order functions (methods) handles a specific pattern of problem. You will get to know the patterns if you keep an eye out while you practice programming. For instance, when a problem involves an array in which we only want to operate on certain elements that meet a criteria, we should use filter(). Recognizing these problem patterns is where real programming power comes from. Not only will it help you solve new problems because you can see how they are similar to other problems you’ve seen before, but you will also be able to quickly understand code that uses these higher-order functions because you will know what problem pattern they are solving. To compare, if we were to use a for loop for every problem, then every solution would use the same tool and it would be much harder to recognize patterns or similar problems. It’s best to have more than just a hammer in a toolbox.

Let’s take a look at the most commonly used built-in array methods which are higher-order functions.1

every()

every(callbackFn)

callbackFn(element) : boolean

The every() method checks if every one of an array’s elements passes a test and returns true if they all pass, otherwise false.

The argument we need to pass to every() is a function that takes in one element and returns a boolean value (true or false). We get to define the function and give it a name, but we don’t get to decide how it gets called. That’s the trick to higher-order functions. Since the higher-order function gets to call our function, it already has an idea in mind of how it’s going to be called and what arguments to pass to it. So we just have to know that our function’s argument will become each element of the array and our function needs to return a boolean value.

In this case, every() is the higher-order function. The function that we pass to it as an argument is called a callback function. The every() method is expecting the callback function to take a single argument and return a boolean value, so that’s exactly how our callback function has to work.

const isEven = x => x % 2 === 0;
console.log(isEven(2)); // true
console.log(isEven(5)); // false

const someNumbers = [1, 2, 3, 4, 5];
console.log(someNumbers.every(isEven)); // false

const evenNumbers = [2, 4, 6, 8];
console.log(evenNumbers.every(isEven)); // true

In this example, isEven() is our callback function. It takes a single argument, x, and returns true if x is even, otherwise false. The argument x will become each element of the given array when every() runs, then we will get our final answer.

It is commonplace to define the callback function directly inside of the higher-order function call if we don’t need to keep it for reuse.

const evenNumbers = [2, 4, 6, 8];
console.log(evenNumbers.every(x => x % 2 === 0)); // true

How about checking if every name in an array starts with the letter ‘A’?

const names1 = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve'];
console.log(names1.every(name => name.startsWith('A'))); // false

const names2 = ['Alice', 'Adam', 'April', 'Amy'];
console.log(names2.every(name => name.startsWith('A'))); // true

const names3 = ['Alice', 'Adam', 'April', 'Amy', 'Bob'];
console.log(names3.every(name => name.startsWith('A'))); // false

A whole category of problems just got a lot easier with every()!

some()

some(callbackFn)

callbackFn(element) : boolean

The some() method is like every(), except only one element needs to pass the test for the final result to return true. In other words, if any element passes the test, return true, otherwise false.

Let’s take a look at similar examples as before, using some() instead of every().

const isEven = x => x % 2 === 0;

const someNumbers = [1, 2, 3, 4, 5];
console.log(someNumbers.some(isEven)); // true

const evenNumbers = [2, 4, 6, 8];
console.log(evenNumbers.some(isEven)); // true

const oddNumbers = [1, 3, 5, 7];
console.log(oddNumbers.some(isEven)); // false
const names1 = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve'];
console.log(names1.some(name => name.startsWith('A'))); // true

const names2 = ['Alice', 'Adam', 'April', 'Amy'];
console.log(names2.some(name => name.startsWith('A'))); // true

const names3 = ['Bob', 'Carol', 'Dave', 'Eve'];
console.log(names3.some(name => name.startsWith('A'))); // false

map()

map(callbackFn)

callbackFn(element) : any

Probably the most commonly used of the higher-order array methods is map(). It takes in a function and returns a new array after having applied that function to each element. Because the function we provide to map() is meant to operate on each individual element of the array, we must define it as a function that takes one argument and returns a new value (of any type).

The kinds of problems that map() helps with are difficult to describe in words, so let’s look at some examples.

Let’s say we have an array of numbers:

const someNumbers = [3, 99, 0, -6, 10];

And we want to increase each number by 1. We can first define a function that takes in a single number and returns its value plus 1.

const add1 = x => x + 1;
console.log(add1(5)); // 6

We can try using this function on a single element of our array.

console.log(add1(someNumbers[0])); // 4

If we want to apply the function to each element of our array, we could write it out the long way.

console.log( [
  add1(someNumbers[0]),
  add1(someNumbers[1]),
  add1(someNumbers[2]),
  add1(someNumbers[3]),
  add1(someNumbers[4])
] ); // [ 4, 100, 1, -5, 11 ]

This works, but it requires us to know exactly how many element are in the array. And it would take a lot of code for a long array. Instead, we can use map().

console.log(someNumbers.map(add1)); // [ 4, 100, 1, -5, 11 ]

As before, we can skip defining the add1 function as a variable and just define it in the map() call. The complete example is as follows.

const someNumbers = [3, 99, 0, -6, 10];
console.log(someNumbers.map(x => x + 1)); // [ 4, 100, 1, -5, 11 ]

Another use case is to extract a common property from each element. Let’s say we have an array of names and we want to get the lengths of each name. Since each name is a string and has a length property, we can use map() here.

const names = ['Alice', 'Bob', 'Carol', 'Dean', 'Eve', 'Frank'];
const nameLengths = names.map(name => name.length);
console.log(nameLengths); // [ 5, 3, 5, 4, 3, 5 ]

In general, whenever we have some array xs and some function f (that operates on a single element x), using xs.map(f) gives us [ f(xs[0]), f(xs[1]), f(xs[2]), ... ].

Map is the method to use if we want to:

  • start with an array,
  • do the same thing to each element of the array,
  • and end up with a new array of the same size.

forEach()

forEach(callbackFn)

callbackFn(element) : undefined

The forEach() method is exactly the same as map() except it doesn’t return the new array. It takes in a function to operate on each element of the array, but it simply leaves it at that and doesn’t return anything. This is used for when we want to perform some action on each element instead of ending up with a new array. The simplest example of an action is printing, i.e., console.log().

const names = ['Alice', 'Bob', 'Carol'];

const sayHello = name => {
  console.log(`Hello, ${name}!`);
};

names.forEach(sayHello);

// Without defining the function separately
names.forEach(name => {
  console.log(`Hello, ${name}!`);
});

Notice that our function does not return anything. Even if it did, the return value wouldn’t be used because forEach() won’t keep track of it.

Printing the lengths of an array of names just became easier!

const names = ['Alice', 'Bob', 'Carol', 'Dean', 'Eve', 'Frank'];
names.forEach(name => console.log(name.length));

filter()

filter(callbackFn)

callbackFn(element) : boolean

One of the slightly less common, but still highly useful, higher-order array methods is filter(). It also takes in a function to operate on each element of the array, except the function’s job is to say whether to keep the element or not. After using filter(), we end up with a new filtered array that has only certain elements from the original array. Because we can only either keep an element or not, the callback function should return a boolean value (true or false). The new array will only have the elements for which the function returns true.

const someNumbers = [3, 1, 2, 5, 6, 9, 8];

const isEven = num => num % 2 === 0; // returns true if num is even, otherwise false
console.log(someNumbers.filter(isEven)); // [ 2, 6, 8 ]

const isOdd = num => !isEven(num);
console.log(someNumbers.filter(isOdd)); // [ 3, 1, 5, 9 ]

We can filter our array of names in different ways.

const names = ['Alice', 'Bob', 'Carol', 'Dean', 'Eve', 'Frank'];

console.log(names.filter(name => name.length <= 4)); // [ 'Bob', 'Dean', 'Eve' ]

console.log(names.filter(name => {
  return name.startsWith('A') || name.startsWith('E');
})); // [ 'Alice', 'Eve' ]

With a clever use of some(), we can filter for names that start with some vowel.

const names = ['Alice', 'Bob', 'Carol', 'Dean', 'Eve', 'Frank', 'Ingrid'];
const vowels = ['a', 'e', 'i', 'o', 'u'];

console.log(names.filter(name => {
  return vowels.some(vowel => name.toLowerCase().startsWith(vowel));
})); // [ 'Alice', 'Eve', 'Ingrid' ]

A common use of filter() is to check how many of a certain kind of element exists in an array. For example, how many numbers are even?

const someNumbers = [3, 1, 2, 5, 6, 9, 8];
const isEven = num => num % 2 === 0;

console.log(someNumbers.filter(isEven).length); // 3

Filter is the method to use if we want to:

  • start with an array
  • and end up with a new array of smaller or equal size as the original, without changing the elements.

reduce()

forEach(callbackFn, initialValue)

callbackFn(accumulator, currentValue) : any
initialValue : any

If the previous methods are for handling certain kinds of problems with arrays, then reduce() is for everything else. Being a very expressive method, reduce() can be difficult to master but very useful.

The callback function passed to reduce() needs to take two arguments. Let’s call them acc (short for “accumulator”) and x. Like the other methods, x will become each element of the array. acc gets an initial value, given by the argument after the callback function, then takes on the values of each successive result of the callback function. You can think of reduce() as starting with an array and reducing it to a single value (the final value of acc). Some examples will make this more clear.

We don’t yet have a way to sum a bunch of numbers in an array. This is a great use case for reduce().

const someNumbers = [1, 2, 3, 4, 5];
console.log(someNumbers.reduce((acc, x) => acc + x, 0)); // 15

All we’re doing here is adding each number to an accumulator until we reach the end. To break this down, we’re giving reduce() two arguments: a callback function and the initial value 0. In our callback function, acc starts off as 0. The first call of our function uses the first element, 1, as the value for x, so we get acc + x = 0 + 1 = 1 as a result. Then the new value for acc is 1 (the previous result). Then x becomes the next element of the array, 2. This time, we get acc + x = 1 + 2 = 3 as a result. Then the new value for acc is 3. And repeat, x becomes 3. Now, acc + x = 3 + 3 = 6. Then the new value for acc is 6. Repeat, x becomes 4. acc + x = 6 + 4 = 10. Then the new value for acc is 10. Finally, x becomes 5. acc + x = 10 + 5 = 15. Then the value for acc is 15 and that is the final result because there are no more elements in the array.

Perhaps more clearly:

acc x acc + x
0 1 0 + 1 = 1
1 2 1 + 2 = 3
3 3 3 + 3 = 6
6 4 6 + 4 = 10
10 5 10 + 5 = 15
15 N/A N/A

The result we end up with doesn’t have to be the same type as the elements. For example, we can sum the lengths of an array of strings all at once:

const names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve'];
console.log(names.reduce((acc, name) => acc + name.length, 0)); // 20

We can also end up with an array! For example, we can flatten a nested array:

const nested = [ ['Alice', 'Bob'], ['Carol', 'Dave'] ];
console.log(nested.reduce((acc, x) => [...acc, ...x], []));

In fact, reduce() is so expressive that we can define all of the previous methods just by using it alone:

const reduceEvery = (xs, f) => xs.reduce((acc, x) => acc ? f(x) : false, true);
console.log(reduceEvery([1, 2, 3, 4, 5], x => x % 2 === 0)); // false
console.log(reduceEvery([2, 4, 6, 8], x => x % 2 === 0)); // true

const reduceSome = (xs, f) => xs.reduce((acc, x) => acc ? true : f(x), false);
console.log(reduceSome([1, 2, 3, 4, 5], x => x % 2 === 0)); // true
console.log(reduceSome([1, 3, 5, 7], x => x % 2 === 0)); // false

const reduceMap = (xs, f) => xs.reduce((acc, x) => [...acc, f(x)], []);
console.log(reduceMap([1, 2, 3, 4, 5], x => x + 1)); // [ 2, 3, 4, 5, 6 ]
console.log(reduceMap(['Alice', 'Bob', 'Carol'], name => name.length)); // [ 5, 3, 5 ]

const reduceFilter = (xs, f) => xs.reduce((acc, x) => f(x) ? [...acc, x] : acc, []);
console.log(reduceFilter([1, 2, 3, 4, 5], x => x % 2 === 0)); // [ 2, 4 ]
console.log(reduceFilter(['Alice', 'Bob', 'Carol'], name => name.length < 4)); // [ 'Bob' ]

Exercises


  1. See all the array methods on MDN.↩︎

Top