Array methods and iterables - Stepping up your JavaScript game
Published 3/20/2019
This article is part of a series:
1 Array methods and iterables - Stepping up your JavaScript game
2 Explaining shallow / deep copying through acronyms
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.
Check out my e-book!
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 thegroup
key from the user, and puts the remaining values insideuserData
.- 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.