Javascript For Loop Alternatives 1
Javascript is an incredibly flexible programming language being, by its own definition, multi-paradigm. Javascript gives us plenty of ways to approach problems, the Array being one of the most misunderstood and flexible data structures with incredible tools built in.
Arrays are intertwined with the use of for loops, however, there are a suite of alternatives to for loops that make code easier to read and reason about, shorter, cleaner, and safer. We will create examples replacing common uses of for loops with find, filter, slice, map, reduce, and forEach.
This is a two part article, in part 1 we will cover find, filter, and slice. For map, reduce, and forEach please go to Part 2 .
We will be using the following Array of Users with the following structure:
[{ first_name: string, last_name: string, age: number }];
The data set we will be using is :
const Users = [
{ first_name: 'Ollanius', last_name: 'Persson', age: 36 },
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 },
{ first_name: 'Camille', last_name: 'Shivani', age: 47 },
];
Alternative 1: find
The Problem: If you need to find the first matching element in an array, the find method is your best alternative.
Case Example using find
You want to find the user that matches a firstname value you can do so with a simple one-liner using the Array _method find:
const argel = Users.find(user => user.first_name === 'Argel');
console.log(argel); // { first_name: 'Argel', last_name: 'Tal', age: 54 }
The find method is part of every javascript Array, it is a high order function that takes the element as its first argument, and then returns the value of the first element in the array that matches the expression of our function.
In this example we call every element of our Users Array user and look for the first match that has the first_name value of ‘Argel’. We see that what the find method returns, is the complete element from the Array that matched our expression.
Logic operators: We can use any expression we want in find, we could swap out === for any logic operator such as greater than or less than.
const agedOver40 = Users.find(user => user.age > 40);
console.log(agedOver40); // { first_name: 'Garviel', last_name: 'Loken', age: 45 }
For Loop implementation of find
Our instinct when working with arrays is usually to jump into a for loop. We will implement the same logic of our first example to compare the resulting code.
let cyrene;
for (user of Users) {
if (user.first_name === 'Cyrene') {
cyrene = user;
break;
}
}
console.log(cyrene); //{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 }
This is the abbreviated way of using a for loop through the use of the for...of loop. In this example we had to declare the let before starting the loop, then we had to open a for loop where each element of our Users Array is called user, inside we had to use the if statement, and then we had to reach outside of the scope of the for loop to use the let we declared and assign it the matching user value. Then we had to break to stop the for loop from going through the entirety of the Array and possibly overriding the value with another user that matches the statement.
Find method vs for loop results
find method | for..of loop |
---|---|
1 line of code | 7 lines of code |
62 characters | 101 characters |
- Find can be understood on first sight, with the use of our for...of loop we need to read over the code and reason each step to understand what is being done.
- There is more room for error in our for...of loop, if we forget the break statement we will not get a run-time error but we would get unwanted behavior.
- The value we get from find is set to a const which can not be re-assigned, in the for loop we need to use let which can be overwritten in any part of our code.
Alternative 2: filter
The problem: If you need an Array with all the elements that match an expression, filter is your best alternative.
Case Example using filter
We will use the same Users Array where we will find every user aged over 40.
const usersOver40 = Users.filter(user => user.age > 40);
console.log(usersOver40);
/*[
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 }
]*/
The filter method is part of every javascript Array, it is also high order function that takes the element as its first argument. The return of map is a new Array with the elements that pass our expression. If no elements pass, an empty array will be returned.
Here we called the elements of our array user and set filter to find every user that had an age attribute with a value greater than 40.
As we can see, filter and find work very similarly: find stops at finding the first element that matches our expression, filter will find every element that matches and returns them in a new Array. However the syntax is exactly the same as in our second example of find.
For Loop implementation of filter
const usersOver40For = [];
for (user of Users) {
if (user.age > 40) usersOver40For.push(user);
}
console.log(usersOver40For);
/*[
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 }
]*/
In this loop we declare our Array before starting the loop, then we execute the loop and use an if statement that will push into our array if there is a match.
Filter method vs for loop results
find method | for..of loop |
---|---|
1 line of code | 4 lines of code |
56 characters | 97 characters |
- Filter is readable and requires no reasoning beyond the first look.
- We must declare a value before looping and then use an additional array method push in our for...of loop.
Bonus: Re-usability in Array Methods
We re wrote code in our find and filter methods. Since these methods are higher order functions we can pass a function into them, this would let us re-use our code and make it more legible.
We could declare a function that holds the logic we will use in our methods:
const ageOver45 = user => user.age > 45;
We can then use it in both find and filter:
const firstUserOver45 = Users.find(ageOver45);
console.log(firstUserOver45); // { first_name: 'Argel', last_name: 'Tal', age: 54 }
const usersOver45 = Users.filter(ageOver45);
console.log(usersOver45);
/*[
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Camille', last_name: 'Shivani', age: 47 }
]*/
This makes our code extremely re-usable, more human readable, and composable.
Alternative 3: slice
The problem: If you need a portion of an Array or a copy of an Array that won’t modify the original Array, slice is your best alternative.
Case Example using slice
If you want an array of the last 3 users of our Users Array, you can use slice:
const last3Users = Users.slice(Users.length - 3);
console.log(last3Users);
/*[
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 },
{ first_name: 'Camille', last_name: 'Shivani', age: 47 }
]*/
Slice is a HOF as well. It takes two optional arguments, here we pass the first one: start. We give slice the first index of where we want to start the creation of the copy/slice and since we don’t pass the second argument, slice will continue until the end of the array.
If you want a all the users from the second to the fourth:
const users2Through4 = Users.slice(1, 4);
console.log(users2Through4);
/*[
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 }
]*/
Here we use the second optional argument of slice, end, which is where the extraction should end, note that it does NOT include the end index. Since all arrays start with index 0, we set the start at the second position, index 1, and then cut through until the fifth index 4, which is not included in the slice we will extract.
Dynamic Indexes: In our case we know the index where we want to start and end, however usually you might not know where you want to start or end your slice. In the case where you want to use the start and end indexes based on some value in your array, you can select them using the indexOf Array method if using primitive values or findIndex for objects:
const indexOfSlice = Users.slice(
Users.findIndex(user => user.first_name === 'Argel'),
Users.findIndex(user => user.first_name === 'Ignace') + 1
);
console.log(indexOfSlice);
/*[
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 }
]*/
For Loop implementation of slice
We will do the for loop implementation of our second slice example:
let end = false;
const sliceUsingFor = [];
for (let i = 0; end === false; i++) {
if (i >= 1) sliceUsingFor.push(Users[i]);
if (i === 3) end = true;
}
console.log(sliceUsingFor);
/*[
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 }
]*/
Here we use the vanilla for loop where we start at i equals 0, end the loop when end is true, and add 1 to i on each iteration. We need to declare end as false before we start the loop, and also declare our empty array beforehand. Here we will use i as the index to access our Users array. When our first if statement matches, index over or equal to 1, we push the user at the index position into our array. If the index is 3, we set end to true, which will end the loop.
There are plenty more ways to do this using while, or other for loop versions, but this is the one that more closely resembles the process that occurs when we use slice. The less verbose implementation would be:
const sliceForRefactored = [];
for (let i = 1; i < 4; i++) {
sliceForRefactored.push(Users[i]);
}
This will output the same result, but it is still more verbose than slice and less readable and understandable to someone reading your code.
Slice method vs for loop results (short version)
find method | for loop |
---|---|
1 line of code | 4 lines of code |
41 characters | 98 characters |
- Slice is readable and requires no reasoning beyond the first look.
- We must declare a value before looping, reason about how we will use the variable in our for loop, and then use an that value and the additional array method push in our for loop.
Bonus: Immutability and Chaining
Slice can be used to create a shallow copy of an array by passing no arguments to it. A shallow copy is a copy that will not alter the original which happens in cases you wouldn’t expect using javascript.
Mutability Accident Cases
In a case where you want to do various things to an existing Array, you might end up altering the Array on every step, meaning that you will need to keep track of how the Array has changed. This makes your code error prone and much harder to read and understand. Here is where immutability is a great asset by guaranteeing that your initial data set remains unaltered.
const sortedUsersYoungest = Users.sort(
(firstUser, secondUser) => firstUser.age - secondUser.age
);
In this example we used the sort method to sort our Users Array from youngest to oldest. In using sort you would expect our new const sortedUsersYoungest would hold the sorted Array and our original User Array would remain the same. However if we log both Arrays we will see:
console.log(sortedUsersYoungest);
/*[
{ first_name: 'Ollanius', last_name: 'Persson', age: 36 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 },
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Camille', last_name: 'Shivani', age: 47 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 }
]*/
console.log(Users);
/*[
{ first_name: 'Ollanius', last_name: 'Persson', age: 36 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 },
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Camille', last_name: 'Shivani', age: 47 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 }
]*/
If you were to perform a find or slice operation on Users after this, your result would be unexpected because we altered the order of our original Array inadvertently.
You might think that you can copy the array and then sort it like this:
const UsersCopy = Users;
const sortedUsersYoungest = UsersCopy.sort(
(firstUser, secondUser) => firstUser.age - secondUser.age
);
However if we log Users after this operation, we will see that it was also altered. Javascript can get confusing sometimes...
Using slice for Immutability
We can fix this code to not alter the original data set by adding 7 characters to our code by chaining slice:
const sortedUsersYoungest = Users.slice().sort(
(firstUser, secondUser) => firstUser.age - secondUser.age
);
When we use of slice with no arguments we create a shallow copy of our Array. A shallow copy is a copy that is allocated in a different space in memory which will not modify the original value was based on. By using slice and chaining it with sort, when we log we get:
console.log(sortedUsersYoungest);
/*[
{ first_name: 'Ollanius', last_name: 'Persson', age: 36 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 },
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Camille', last_name: 'Shivani', age: 47 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 }
]*/
console.log(Users);
/*[
{ first_name: 'Ollanius', last_name: 'Persson', age: 36 },
{ first_name: 'Garviel', last_name: 'Loken', age: 45 },
{ first_name: 'Argel', last_name: 'Tal', age: 54 },
{ first_name: 'Cyrene', last_name: 'Valantion', age: 36 },
{ first_name: 'Ignace', last_name: 'Karkasy', age: 42 },
{ first_name: 'Camille', last_name: 'Shivani', age: 47 }
]*/
Now we can do any operation we want on Users without having to keep track of what we might have changed in our original Array.
Conclusion
We have dipped our toes in the water of Array method alternatives, baked in to javascript, to the use of for loops always making them much shorter to write, easier to read, easier to reason about, and safer. In this post we covered three of the javascript Array methods: find, filter, and slice, use cases for each of them, and additional benefits they can have such as reusability of code, chaining, and immutability.
In the second part of this post, we will cover three other more complex Array methods: map, reduce, and forEach along with examples and use cases.