Array methods and iterables - Stepping up your JavaScript game

michi

Michael Z

Posted on March 20, 2019

Array methods and iterables - Stepping up your JavaScript game

Originally posted at michaelzanggl.com. Subscribe to my newsletter to never miss out on new content.

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 }
]
Enter fullscreen mode Exit fullscreen mode

Throughout this article you will understand how to turn this

const activeUsernames = []

users.forEach(user => {
  if (user.active) {
    activeUsernames.push(user.name)
  }
})
Enter fullscreen mode Exit fullscreen mode

into this

const activeUsernames = users
  .filter(user => user.active)
  .map(user => user.name)
Enter fullscreen mode Exit fullscreen mode

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++) {
    //
}
Enter fullscreen mode Exit fullscreen mode

Array.prototype.forEach

users.forEach(function(user) {
    //
}
Enter fullscreen mode Exit fullscreen mode

ES6 for of Loop

for (const user of users) {
    //
}
Enter fullscreen mode Exit fullscreen mode

One more thing before we get started!

If you are unfamiliar with ES6 arrow functions like:

users.map(user => user.name)
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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)
  }
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Another use for filter is the following

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

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)
})
Enter fullscreen mode Exit fullscreen mode

Mapping it

const usernames = users.map(user => user.name)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
  }
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Solution using some

users.some(user => user.active)
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Solution using every

users.every(user => user.active)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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' },
  ],
}
Enter fullscreen mode Exit fullscreen mode

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
}, {})
Enter fullscreen mode Exit fullscreen mode
  • 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++) {
    //
}
Enter fullscreen mode Exit fullscreen mode

Array.prototype.forEach: method on the array prototype

users.forEach(function(user) {
    //
}
Enter fullscreen mode Exit fullscreen mode

ES6 for of Loop: works on iterables

for (const user of users) {
    //
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But it is an array like object

> document.querySelectorAll('.contentinfo')

    NodeList [div#license.contentinfo]
        0: div#license.contentinfo
        length: 1
        __proto__: NodeList
Enter fullscreen mode Exit fullscreen mode

and is also iterable

Symbol.iterator in Object(document.querySelectorAll('div')) //? true
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

or

const array = [...document.querySelectorAll('div')]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

is definitely much more readable than

const usernames = []

users.forEach(user => {
  usernames.push(user.name)
})
Enter fullscreen mode Exit fullscreen mode

wouldn't

const usernames = users.pluck('name')
Enter fullscreen mode Exit fullscreen mode

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.


If this article helped you, I have a lot more tips on simplifying writing software here.

💖 💪 🙅 🚩
michi
Michael Z

Posted on March 20, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related