Array methods and iterables - Stepping up your JavaScript game

Published 3/20/2019

This article is part of a series:

Today I want to introduce some array methods that help you step up your JavaScript game.

For all examples, let's imagine we have the following variable declaration

let users = [
  {id: 1, name: 'Michael', active: true, group: 1 }, 
  {id: 2, name: 'Lukas', active: false, group: 2 }
]

Throughout this article you will understand how to turn this

const activeUsernames = []

users.forEach(user => {
  if (user.active) {
    activeUsernames.push(user.name)
  }
})

into this

const activeUsernames = users
  .filter(user => user.active)
  .map(user => user.name)

as well as a lot more.

We want to focus on four objectives when it comes to code improvements

  • avoiding temporary variables
  • avoiding conditionals
  • be able to think of your code in steps
  • reveal intent

We will highlight the most important methods on the Array prototype (leaving out basic array manipulation like push, pop, splice or concat) and hopefully you will find scenarios where you can apply these instead of the following usual suspects.

for loop

for (let i = 0; i < users.length; i++) {
    //
}

Array.prototype.forEach

users.forEach(function(user) {
    //
}

ES6 for of Loop

for (const user of users) {
    //
}

One more thing before we get started!

If you are unfamiliar with ES6 arrow functions like:

users.map(user => user.name)

I recommend you to take a look at those first. In summary, the above is very similar, and in this case, the same as

users.map(function(user) {
   return user.name
})

Array.prototype.filter

Let's say we want to find all users that are active. We briefly looked at this in the introduction of the article.

const activeUsers = []

users.forEach(user => {
  if (user.active) {
    activeUsers.push(user)
  }
})

If we look back at the four objectives we set before, it is very obvious that this is violating at least two of them. It has both temporary variables as well as conditionals.

Let's see how we can make this easier.

const activeUsers = users.filter(user => user.active)

The way Array.prototype.filter works is that it takes a function as an argument (which makes it a higher order function) and returns all users that pass the test. In this case all users that are active.

I think it is safe to say that we were also able to reveal our intent. forEach can mean anything, it might save to the database, etc. while filter does what the name suggests.

Of course you can also use filter on a simple array. The following example would return all animals starting with the letter a.

['ape', 'ant', 'giraffe'].filter(animal => animal.startsWith('a'))

A use case I also see often is removing items from an array. Imagine we delete the user with the id 1. We can do it like this

users = users.filter(user => user.id !== 1)

Another use for filter is the following

const result = [true, 1, 0, false, '', 'hi'].filter(Boolean) 
result //? [true, 1, 'hi']

This effectively removes all falsy values from the array. There is no magic going on here. Boolean is a function that takes an argument to test whether it is truthy or not. E.g. Boolean('') returns false, while Boolean('hi') returns true. We simply pass the function into the filter method, so it acts as our test.

Array.prototype.map

It often happens that we have an array and want to transform every single item in it. Rather than looping through it, we can simply map it. Map returns an array with the same length of items, it's up to you what to return for each iteration.

Let's create an array that holds the usernames of all our users.

Traditional loop

const usernames = []

users.forEach(user => {
  usernames.push(user.name)
})

Mapping it

const usernames = users.map(user => user.name)

We avoid temporary variables and reveal intent at the same time.

Chaining

What is great about these higher order functions is that they can be chained together. map maps through an array and returns a new array. filter filters an array and returns a new array. Can you see a pattern? With this in mind, code like the following becomes not only possible but very readable

const activeUsernames = users
  .filter(user => user.active)
  .map(user => user.name)

And with this we complete our final objective to think in steps. Rather than thinking out the whole logic in your head, you can do it one step at a time. Think of the example we had at the very start.

const activeUsernames = []

users.forEach(user => {
  if (user.active) {
    activeUsernames.push(user.name)
  }
})

When you read this for the first time, in your mind the process would go somewhat like

- initialize an empty array
- loop through all users
    - if user is active
        - push to the array from the beginning
            - but only the name of the user
- repeat

With the refactored method it looks more like this

- get all active users
- create new array of the same size
    - that only hold their username

That's a lot easier to think and reason about.


There are many more interesting methods available. Let's check out some more.

Array.prototype.find

The same way filter returns an array with all the items that pass the test, find returns the first item that passes the test.

// returns user with id 1
users.find(user => user.id === 1)

Array.prototype.findIndex works the same way but it returns the index instead of the item

For arrays that don't require deep checking there is no need to have the overhead of an extra function, you can simply use includes and indexOf respectively.

['a', 'b', 'c'].includes('b') //? true
['a', 'b', 'c'].indexOf('a') //? 0
['a', 'b', 'c'].includes('d') //? false
['a', 'b', 'c'].indexOf('d') //? -1

Array.prototype.some

Returns true if at least one test passes. We can use this when we want to check if at least one user in our array is active.

Traditional solution using for loop

let activeUserExists = false
for (let i = 0; i < users.length; i++) {
  if (users[i].active) {
    activeUserExists = true
    break
  }
}

Solution using some

users.some(user => user.active)

Array.prototype.every

Returns true if all items pass the test. We can use this when we want to check whether all users are active or not.

Traditional solution using for loop

let allUsersAreActive = true
for (let i = 0; i < users.length; i++) {
  if (!users[i].active) {
    allUsersAreActive = false
    break
  }
}

Solution using every

users.every(user => user.active)

Array.prototype.reduce

If none of the above functions can help you, reduce will! It basically boils down the array to whatever you want it to be. Let's look at a very simple implementation with numbers. We want to sum all the numbers in the array. In a traditional forEach loop it would look like this:

const numbers = [5, 4, 1]
let sum = 0
numbers.forEach(number => sum += number)
sum //? 10

But the reduce function takes away some of the boilerplate for us.

const numbers = [5, 2, 1, 2]
numbers.reduce((result, number) => result + number, 0) //? 10

reduce takes two arguments, a function and the start value. In our case the start value is zero. If we would pass 2 instead of 0 the end result would be 12.

So in the following example

const numbers = [1, 2, 3]
numbers.reduce((result, number) => {
    console.log(result, number)
    return result + number
}, 0)

the logs would show:

  • 0, 1
  • 1, 2
  • 3, 3

with the end result being the sum of the last two numbers 3 and 3, so 6.

Of course we can also reduce our array of objects into, let's say a hashmap.

Grouping by the group key, the resulting hashMap should look like this

const users = {
  1: [
    { id: 1, name: 'Michael' },
  ],
  2: [
    { id: 2, name: 'Lukas' },
  ],
}

We can achieve this with the following code

users.reduce((result, user) => {
  const { group, ...userData } = user
  result[group] = result[group] || []
  result[group].push(userData)
  
  return result
}, {})
  • const { group, ...userData } = user takes the group key from the user, and puts the remaining values inside userData.
  • With result[group] = result[group] || [] we initialize the group in case it doesn't exist yet.
  • We push userData into the new group
  • We return the new result for the next iteration

Using this knowledge on other iterables and array-like objects

Do you remember this from before?

for loop: works on array-like objects

for (let i = 0; i < users.length; i++) {
    //
}

Array.prototype.forEach: method on the array prototype

users.forEach(function(user) {
    //
}

ES6 for of Loop: works on iterables

for (const user of users) {
    //
}

Did you realize how significantly different the syntax of the forEach and the two for loops are?

Why? Because the two for loops do not only work on arrays. In fact they have no idea what an array even is.

I am sure you remember this type of code from your CS classes.

const someString = 'Hello World';
for (let i=0; i < someString.length; i++) {
    console.log(someString[i]);
}

We can actually iterate through a string even though it is not an array.

This kind of for loop works with any "array like object", that is an object with a length property and indexed elements.

The for of loop can be used like this

const someString = 'Hello World';
for (const char of someString) {
    console.log(char);
}

The for of loop works on any object that is iterable.

To check if something is iterable you can use this rather elegant line Symbol.iterator in Object('pretty much any iterable').

This is also the case when dealing with the DOM. If you open the dev tools right now and execute the following expression in the console, you will get a nice red error.

document.querySelectorAll('div').filter(el => el.classList.contains('text-center'))

Unfortunately filter does not exist on iterable DOM collections as they are not Arrays and therefore don't share the methods from the Array prototype. Want proof?

(document.querySelectorAll('div') instanceof Array) //? false

But it is an array like object

> document.querySelectorAll('.contentinfo')

    NodeList [div#license.contentinfo]
        0: div#license.contentinfo
        length: 1
        __proto__: NodeList

and is also iterable

Symbol.iterator in Object(document.querySelectorAll('div')) //? true

If we want to use our newly trained Array knowledge on let's say iterable DOM collections, we have to first turn them into proper arrays.

There are two ways of doing it.

const array = Array.from(document.querySelectorAll('div'))

or

const array = [...document.querySelectorAll('div')]

I personally prefer the first way as it provides more readability.

Conclusion

We went through the most important methods on the array object and took a look at iterables. If we look back to the objectives we set at the start, I think it is safe to say that we at least accomplished

  • thinking in steps
  • avoiding temporary variables
  • avoiding conditionals

But I am not fully satisfied with reveal intent.

While

const usernames = users.map(user => user.name)

is definitely much more readable than

const usernames = []

users.forEach(user => {
  usernames.push(user.name)
})

wouldn't

const usernames = users.pluck('name')

be even nicer?

In the next article, we will take a look at subclassing arrays, so we can provide exactly such functionality. It will also be a great entry point for unit testing with Node.js, so stay tuned.

P.S. if you are a fan of Laravel, please take a look at Laravel Collections.