Javascript For Loop Alternatives 2
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 covered the basics: find, filter, and slice. In this article we will cover the more advanced map, reduce, and forEach.
If you don't have a clear understanding of find, filter, and slice, you will find this article confusing.
We will be using the following data set of items throughout the cases:
const items = [
{ name: 'chainsword', price: 20, stock: 5 },
{ name: 'bolter', price: 15, stock: 10 },
{ name: 'chainaxe', price: 20, stock: 2 },
{ name: 'plasma pistol', price: 25, stock: 7 },
{ name: 'heavy bolter', price: 30, stock: 3 },
];
Alternative 4: map
The Problem: If you need to perform any kind of operation on every item in an array, the map method is your best alternative.
Case Example using map
Map is one of the most widely used Array methods because it is extremely versatile, powerful, and doesn't alter the original Array.
Lets say you want to see what the item price would be with a VAT of 20% (1% more than where I live ;D) you could do so with a one liner:
const vat = 1.2; // We could get this value from the UI through one of many means.
const itemsWithVat = items.map(item => ({ ...item, price: item.price * vat }));
/*[
{ name: 'chainsword', price: 24, stock: 5 },
{ name: 'bolter', price: 18, stock: 10 },
{ name: 'chainaxe', price: 24, stock: 2 },
{ name: 'plasma pistol', price: 30, stock: 7 },
{ name: 'heavy bolter', price: 36, stock: 3 }
]*/
Map is a higher order function that takes the element as its first argument, and then returns a new array with the results of the function we passed.
In this case we used the spread operator to return the full object plus the new price property value after our VAT calculation. If we just wanted an array of the prices with VAT we would just run:
const pricesWithVat = items.map(item => item.price * vat);
//[ 24, 18, 24, 30, 36 ]
The full object implementation is much more commonly used, especially in front end development.
For Loop implementation of map
const itemsWithVatFor = [];
for (const item of items) {
itemsWithVatFor.push({ ...item, price: item.price * vat });
}
console.log(itemsWithVatFor);
/*[
{ name: 'chainsword', price: 24, stock: 5 },
{ name: 'bolter', price: 18, stock: 10 },
{ name: 'chainaxe', price: 24, stock: 2 },
{ name: 'plasma pistol', price: 30, stock: 7 },
{ name: 'heavy bolter', price: 36, stock: 3 }
]*/
In the for...of loop implementation we must declare the Array before starting the for loop, then we iterate over all the items and push the new object we created using the spread operator.
Map method vs for loop results
map method | for..of loop |
---|---|
1 line of code | 4 lines of code |
79 characters | 118 characters |
- Map 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: Front-end use of map
You will see map being used widely in front-end frameworks like React where you have an array of data that you want to create HTML with. This is an example using React's JSX syntax.
{data.items.map(item => (
<div className='item-card'>
<h2 className='item-title'>{item.name}</h2>
<p>
<span className='item-price'>${item.price} </span>
<span className='item-stock'>{item.stock} remaining in stock</span>
</p>
</div>
))}
In this crude example we hypothetically fetch our items from a database, the response is data, and we get our items inside that data. Then we map over them and render a item card with every item in our array, effectively using map to return HTML from JSON data through JSX. This is a very common pattern, and even if you don't know React you can see what is being done.
Alternative 5: reduce
The Problem: If you need to compute a single value using every item in an array, the reduce method is your best alternative.
Case Example using reduce
If you want to know how much money you expect from selling every item in your stock, you would use reduce. Reduce is one of the most complex and confusing array methods, but we will break it down for you:
const totalValue = items.reduce(
(total, item) => (total += item.price * item.stock),
0
);
console.log(totalValue); //555
The reduce method takes two mandatory arguments, the first one is called the accumulator, the second is the currentValue. There is also an optional second argument to the callback function called initialValue which we used in our example as 0.
In our callback, the accumulator is the value we will return at the end of the execution, in our case it is the total value of our stock. The currentValue is the current element being processed in the array, which we have called item, because we are iterating over our items array. We are computing a single number, therefore our initialValue is set at 0 and will be passed as the first accumulator value in our callback.
If we don't provide a initialValue argument, the first element in the array will be used as the default accumulator.
As reduce iterates over our array, it will add on to our total using every item in our array starting at 0. We will track the first 3 iterations of this case of reduce to see how it will work:
//Iteration 1: Uses our initialValue of 0
(0, item) => (0 + item.price * item.stock) //total = 0 + 20 * 5 = 100
//Iteration 2: Uses our new total of 100
(100, item) => (100 + item.price * item.stock) //total = 100 + 15 * 10 = 250
//Iteration 3: Uses our new total of 250
(250, item) => (250 + item.price * item.stock) //total = 250 + 20 * 2 = 290
This process will continue until every item in the array is iterated over and we get our final result.
Bonus: Refactor using object destructuring
I chose to write this example with these names to make it clear what we are talking about, however we can refactor this using destructuring in our item:
const totalValue = items.reduce(
(total, { price, stock }) => (total += price * stock),
0
);
Here we extract the price and stock value from our item object, which allows us to use price and item without having to do item.price or item.stock syntax. This may not always be shorter, but it makes it cleaner to read if you understand what is being destructured.
For Loop implementation of reduce
let totalFor = 0;
for (const item of items) {
totalFor += item.price * item.stock;
}
console.log(totalFor); //555
In our for...of loop implementation of reduce, we declare our total outside of our loop, and we have to declare it as a let so that we can accumulate on it. Inside our loop we call the items and add their price time stock to the total. The major drawback is that we have to use a let, which will make our total value mutable by any other line of code in the future, which is something we might end up using accidentally.
Bonus: Refactor using object destructuring
We can use object destructuring in for...of loops as well. Personally, I value readability over everything when coding, a good codebase is one that you can come back to after a few weeks and understand what it does. In most cases destructuring will do this and save you a lot of code writing.
let totalFor = 0;
for (const { price, stock } of items) {
totalFor += price * stock;
}
console.log(totalFor); //555
Since a for loop is so generic, destructuring really helps make the intention explicit from the start, you can tell from first glance that you are going to use the price and stock properties for the elements in an array of items, and inside you can see that we use those two values to calculate the total, making it clearer on first glance than our previous example.
Map method vs for loop results
reduce method | for..of loop |
---|---|
4 lines of code | 4 lines of code |
94 characters | 87 characters |
- Reduce is more explicit if you have a clear grasp of what reduce does.
- We must declare a value before looping which will be mutable in any other part of our code.
Note on code length
Reduce can be made into a one line easily, however, it is less readable. However this is how it would look like:
const totalValue = items.reduce((x, y) => (x += y.price * y.stock), 0);
This would make our reduce 1 line and 71 characters long at the cost of readability.
Alternative 6: forEach
The Problem: If you need to execute anything not necessarily related to your Array, you can use forEach.
Case Example using forEach
There is nothing that forEach can do that a regular for loop cannot do, except do one liner console.log() statements. The additional benefit is that forEach can be chained where for loops cannot. There are no immutability benefits to the use of forEach, and it is down to a matter of preference between syntax.
items.forEach(item => console.log(item));
forEach has 1 mandatory argument, the currentValue which is the element being processed in the array. Additionally it can use the index, and array which is the full array forEach was called on.
One place where using the index is beneficial is when doing DOM Manipulation using Plain JS. Sometimes you might have a button for each item which you want to add a listener to, and this can be done using a for loop, but the forEach alternative is more readable and shorter.
const itemCards = document.getElementsByClassName('item-card')
const addToCartButtons = document.getElementsByClassName('item-add-button')
Array.from(itemCards).forEach((item, index)=> {
const button = addToCartButtons[index];
item.addEventListener((...) => { ... })
button.addEventListener((...) => { ... })
})
For Loop implementation of forEach
for (const item in items) {
console.log(item);
}
To be honest, this is the use case you will more commonly replace with forEach.
forEach method vs for loop results
I will leave out the comparison since forEach is not actually a useful Array method for iteration like the other methods we have seen. forEach is generic, doesn't create a shallow copy and has more limited functionality than a vanilla for loop or for...of loop. As a general rule, there is always a better alternative to forEach and for loops.
Chaining methods
In part two we saw map and reduce which are more complex Array methods than what we saw in part 1. In the case of reduce, we actually ended up with more code than using for...of, however, chaining is where the real magic happens.
Case Example chaining filter, map, reduce
We want to find out what items we have 5 or more items of, and what their total value is. Additionally we want to capitalize the names to display this data to a shopping cart total. I will inline all of the logic for this example.
const totalAndItems = items
.filter(item => item.stock >= 5)
.map(item => ({
...item,
name: item.name[0].toUpperCase() + item.name.slice(1),
}))
.reduce(
({ total, items }, item) => ({
total: (total += item.price * item.stock),
items: [...items, item.name],
}),
{ total: 0, items: [] }
);
console.log(totalAndItems);
//{ total: 425, items: [ 'Chainsword', 'Bolter', 'Plasma pistol' ] }
console.log(items);
/*[
{ name: 'chainsword', price: 20, stock: 5 },
{ name: 'bolter', price: 15, stock: 10 },
{ name: 'chainaxe', price: 20, stock: 2 },
{ name: 'plasma pistol', price: 25, stock: 7 },
{ name: 'heavy bolter', price: 30, stock: 3 }
]*/
Here we first filtered the items by their stock, then we mapped over the filtered items and returned the whole array with the names Capitalized, where we also used slice, and then we used reduce to get the total value of the items and a list of the names of the items. We can also see that we did not alter our original data set.
For loop implementation of filter, map, reduce
const itemsOver5 = [];
const totalAndItemsFor = { total: 0, items: [] };
for (const item of items) {
if (item.stock >= 5) itemsOver5.push(item);
}
for (const item of itemsOver5) {
item.name = item.name[0].toUpperCase() + item.name.slice(1);
totalAndItemsFor.total += item.price * item.stock;
totalAndItemsFor.items.push(item.name);
}
console.log(totalAndItemsFor);
//{ total: 425, items: [ 'Chainsword', 'Bolter', 'Plasma pistol' ] }
console.log(items);
/*[
{ name: 'Chainsword', price: 20, stock: 5 },
{ name: 'Bolter', price: 15, stock: 10 },
{ name: 'chainaxe', price: 20, stock: 2 },
{ name: 'Plasma pistol', price: 25, stock: 7 },
{ name: 'heavy bolter', price: 30, stock: 3 }
]*/
As a personal note: Writing this code I made a lot more mistakes than writing method chaining and had to run a lot of console.logs to see what was going on. I first tried to do it all in a single for loop, which can definitely be done, but would be far more complex than this example. I made a few frankensteins writing this and I'd estimate it took me about 5 times more time to write this code and troubleshoot it, even having already done the logic in the previous example.
Here we had to declare our filtered array and total before starting anything. Then we had to loop over items to create the filtered array. Once that was done we iterated over that array and uppercased the names and then accumulated the total and pushed the item name to create that list.
In the last line of code we can see that we altered our original data set when we capitalized our filtered array, which is an unexpected side-effect that affects any code we might do using our original array in the future. To avoid this we must be aware that it happens and create a shallow copy by our own where we need it. I'd avoid it all together using for loop alternatives and remove that possibility for good.
Bonus: Refactor using composition
By declaring functions to call in our methods your code will be much more modular and readable:
const stockOver5 = item => item.stock > 4;
const capitalizeWord = word => word[0].toUpperCase() + word.slice(1);
const capitalizeNames = item => ({ ...item, name: capitalizeWord(item.name) });
const getTotalAndItemList = ({ total, items }, item) => ({
total: (total += item.price * item.stock),
items: [...items, item.name],
});
const totalAndItemsRefactored = items
.filter(stockOver5)
.map(capitalizeNames)
.reduce(getTotalAndItemList, { total: 0, items: [] });
console.log(totalAndItemsRefactored);
//{ total: 425, items: [ 'Chainsword', 'Bolter', 'Plasma pistol' ] }
Here we can see that we are filtering stock over 5, then capitalizing the names, and the getting the total and item list just by reading through that last line of code. If we want to see with more detail what we are doing, we can go to the declarations for further clarification.
We can easily declare a new function to call filter using anything else, and we would only need to swap out what we call in filter, additionally, if we need to capitalize any other word in the future, we already have a captializeWord function that does it. This makes our code reusable and modular.
Conclusion
We have covered the main Array method alternatives, baked in to javascript, to the use of for loops. Our code is now always easier to read, easier to reason about, and safer. In most cases it will also be shorter to write. In this post we covered three of the javascript Array methods: map, reduce, and forEach, use cases for each of them, and additional benefits they can have such as reusability of code, chaining, and immutability. We also did a test chaining methods from part 1 and part 2. Get familiar with them and start writing better code!