const products = ['beans', 'pizza', 'chicken', 'salmon']
// Change the name of each element by adding an asterisk next to it
const mappedProducts = products.map(product => `${product} *`)
console.log(mappedProducts)
// ['beans *', 'pizza *', 'chicken *', 'salmon *']
In this “JavaScript iteration in the real world” series of articles, I want to share with you some of the most common JavaScript iteration methods using examples that resemble as closely as possible what their usage looks like in the real world. I will try to avoid `foo` and `bar` or overly complex examples. There won’t be a lot of mentions of your pets and friends either I’m afraid. Instead, we’ll take a deep look at how the method works, why we need it, and what it would look like in a typical modern front-end codebase.
In the previous installment, we talked about the filter()
method in detail. In this article we will take the example we built and show how map()
can help us. You can read the previous article here, but you don’t need it to follow along with the examples in this article.
map()
So let’s explore the Array.map()
method; this is a very powerful method that allows us to do a lot with our arrays. It is frequently used in all types of front-end codebases. Once you learn how it works you’ll find yourself reaching for it time and again to solve many common array processing patterns. It takes an array of elements and returns a new array after transforming the elements based on a function we supply it.
So in the example above we have an array of products and we used `map()` to add an asterisk at the end of each name. Let’s take a closer look at it and see what this line really means.
const mappedProducts = products.map(product => `${product} *`)
In plain English, this line of code means: given an array called products, take each element in the array and add an asterisk to it. Then return a new array with all the transformed elements. When finished, store the new array in a variable called mappedProducts
.
Of course, adding an asterisk to a name is a quick way to show how `map()` works, but doesn’t look realistic at all. So let’s bring “the real world” into our explanation.
In the real world
Like in the previous article, we will continue with the realistic context on which to base our examples. We are working on the codebase of an online shop. The shop displays the products and their corresponding prices.
A realistic thing we might need to do in an online shop is to apply a discount on our prices. Let’s say the holiday season has recently passed and to bump our sales we want to apply a 10% discount across the board on all of our products. That’s the kind of task where map()
shines.
We will continue using the simple app I created especially to demonstrate this in action. You can check out the web app and see map in action, and you can see the code in its GitHub repo. But we will go through everything here first.
Let’s start simple and then build up to what the final code will look like in our example with the 10% discount applied to all our prices. Let’s just think of the prices of our products on their own for a minute. Let’s invent an array of prices. Then say we simply want to knock off 1€ of each price.
const prices = [2, 5, 7, 16, 50, 38]
const discountAmount = 1 // the 1€ we want to discount
const discountedPrices = prices.map(price => price - discountAmount)
console.log(discountedPrices)
// [1, 4, 6, 15, 49, 37]
This is map()
in action. It took our array of prices and mapped over each price in the array. It applied the function we provided to each price. The function subtracts 1 from the original price. Then, and this is very important, it returned a completely new array with the results. The original prices array remains intact as it is. But map()
has given us a new array with the results of the function. We store the new array in a variable discountedPrices
, and now we can use this new array where we need it to show the new prices in our shop.
However, in our shop, we won’t get an array that only has prices. We have an array of products, and each product is an object that has several properties. Price is one of those properties.
// Each product is one object. Products is an array of these objects
{
name: "Hearty beans",
price: "1.99€",
market: "Local Market",
}
Now we are dealing with an object, and not a simple element in the original array. But fear not, `map()’ can handle this without breaking a sweat. We can map over products and apply a discount to our prices by accessing the price property on each product like this:
const
discountedPrices = products.map(product => product.price - 1)
This will reach into the product object and pick out the price property to apply the discount on. But, there is a problem. This will return a new array which only contains the new prices, and we have lost all the other properties of the product object. We need all those properties to render our products correctly in the shop with their name and everything else. So we will need to copy all the properties of each product, modify only the price, then return the new product object. This way we will end up with an array of products identical to the original except for the modified price.
const discountedProducts = products.map(product => ({
...product,
price: product.price - 1,
})
Now instead of returning a simple number on each iteration of the loop, we are returning an object. Let’s take a quick look at what is happening here, even if it is not directly related to how map()
works. We can see we are returning an object because of the curly braces `{}`, and in order not to confuse those with the normal function curly braces we enclose them in round brackets `()`. Inside the object, we use the spread operator `...` to take all the properties from inside `product` and copy them into our new object. Then we modify the one property we are interested in `price`. This returns a new object with the modified property and adds it to the new array.
Now, this new product is added to our new array, and map picks up the next product from the original array and does the same to it too. And so on for each element in the array. Great, so far so good. We have our new product object with the new price. But we didn’t want to subtract 1€ randomly, we wanted to apply a 10% discount. So all we have to do now is get the formula that does this.
const
discountedProducts = products.map(product => ({
...product,
price: (product.price - (product.price * 10) / 100).toFixed(2)
})
We don’t care much about how this formula works. The important bit is to see how map is creating a new array of products for us out of the old array and applying the discount formula to the price of each. If we apply this to our store, you will see how all the prices displayed change, but everything else remains the same.
How does it work
Like filter()
, map()
is another one of the built-in array methods available in JS, and it is a higher-order function too. We access array methods with the syntax ourArray.map()
.
A higher-order function means it is a function that we can pass another function for it to do something with. The function we supply, referred to as a callback function, is passed as an argument. So it’s the first argument we pass within the round brackets of the method map(ourFunction)
. And this callback function will then be applied to each element of the array we are iterating. Each element individually will be transformed and saved to the new array. The callback function can do anything we need it to do.
Once we pass our function to the map method, this provides us with three fixed arguments for our function. We will look only at the first for now.
// We pass our function as an argument to map, this gives our function three arguments to work with
const ourFunction = (element, index, array) => // do stuff
prices.map(ourFunction)
The current element that map is working on is provided to us as the first argument. That’s the singular “product” or “price” we see in our examples above. It is a variable, and we can name it whatever we want. But it’s logical that for an array of products a single element of the array is one “product”, it’s a conventional way of naming the item out of an array of items.
We can use this variable in the body of our callback function. That’s after the arrow of the arrow function here, where we can use this product variable (or price, or whatever else it is) to implement the function we want map()
will then iterate through the whole array and return the new array with all the transformed elements.
Remember that the original array stays intact and doesn’t get modified. We end up with two arrays, the original and the new one. This allows us to have access to both sets at the same time adding to the power of map()
. It’s a recurrent pattern in our front-end apps where we receive an array of complex objects where some transformations need to be applied to it to display one set of data or the other or even both. That’s why you’ll find yourself using map()
regularly.
What’s more
We have just seen how map
provides us with one argument that is the currently selected element from the array. It also provides us with two more predefined arguments that are useful. The second argument is the index. While the third is the array.
myArray.map((element, index, array) => //Do stuff)
The `element`, as we have seen, is the current element being processed. The index is the position of this currently selected element in the array, and the final argument is the original array itself we are iterating on.
For example, this is what we will get if we log each product and its index on each iteration of filter()
const
products = ['beans', 'pizza', 'chicken', 'salmon']
products.map( (product, index) => console.log(product, index))
// 'beans’, 0
// 'pizza’, 1
// 'chicken’, 2
// 'salmon’, 3
Two things to keep in mind. If we are working only with a single argument the round brackets surrounding them are optional, so if you see `element` without the round brackets it’s the same thing, it just looks cleaner.
products.map( (product) => `${product} *`)
// same as
products.map(product => `${product} *`)
But we can only drop the round brackets if we are using only one argument. We need to keep them if we are using more than one.
Secondly, we called the second argument “index” here, and the third argument “array”. But like “product” these are also variable names. You can name them what you want. You can call them “lito” and “hernando” and nothing would change. Remember it’s not the name that matters here, but the position of the argument. The first is always the element, the second is always the index, and the third is the array we started with. However, your obscure pop culture reference notwithstanding, we tend to name things in a way that makes it easier for readability. So the second argument is almost always called “index” or “i” by convention, and the third usually is “array” or “arr”.
So far we have only seen map functions that are implicitly returning directly what comes after the arrow of the arrow function. But what happens if the function we want to supply is more complex and needs to be written on several lines? Then we have to add curly braces to enclose the code in the body of the function and explicitly specify what to return. Once we put curly braces after the arrow function it no longer implicitly returns anything, and that will produce an error. So we have to explicitly tell it what to return using the `return` keyword. What we return is what it will add to the new array.
const discountedProducts = products.map((product) => {
const discountedPrice = product.price - (product.price * 10) / 100
return {
...product,
price: discountedPrice.toFixed(2),
}
})
The example above does exactly the same as our original which adds a 10% discount. But in this case, we decided to write it out on several lines for readability, saving the discounted price in a variable. And as you can see we added the curly braces, and the explicit `return`.
Another way you will see `map()` being written is to write the function we are using separately (either storing it in a variable or with the `function` keyword) and then pass it as a callback function directly. This would look like this.
const subtractOne = price => price - 1
const discountedPrices = prices.map(subtractOne)
One last thing to remember, and that’s when not to use map()
. We should use it only when we want to use the new array it returns. If we want to do something to each element but we don't need the new array, then this is not the right method for the job. We should instead use something like forEach
. I will cover this one in a new upcoming article of this series.
What it looked like prior to ES6
With ES6 we have arrow functions and a lot of great stuff that make our code more readable. But it is important to know what things looked like before because you will come across this way of writing it in tons of older codebases that are still in use, and you will come across it in older examples on Stack Overflow and the like. This is how we wrote a map before ES6:
prices.map(function(price) {
return price - 1
})
As you can see, before we got the arrow function we needed the `function` keyword, the arrow is gone and we need the curly braces and the `return` keyword.
What it looks like in a for loop
Let's pull away from the Wizard of Oz’s screen and look at what’s actually happening behind the scenes. Array methods are higher-order functions that simply have a loop at their heart doing all the actual work. We won’t need to write a for loop for something like this because that’s what `map()` is for. But it’s a good exercise to see what it looks like to understand what’s happening.
function applyDiscount(prices) {
let discountedPrices = []
for(let i = 0; i < prices.length; i++) {
discountedPrices.push(prices[i] - 1)
}
return discountedPrices
}
And if we go one step further and take a look at what map()
itself looks like under the hood we will see something like this.
// callback is the function we create and pass into map
Array.prototype.map = function(callback) {
// this is the new array where we push the transformed elements
let arr = []
for (let i = 0; i < this.length; i++) {
// you can see the three arguments our callback function receives
arr.push(callback(this[i], i, this))
}
// once the loop is complete, return the results pushed into the new array
return arr
}
This is a simplified version of the implementation of map (based on an example I got from here) but serves to show you exactly what is happening when you call the map method, and when you pass the callback function, and what it returns at the end of the iteration.
Conclusion
Armed with a deeper understanding of what map()
is and why we need it and with the context of an example from the real world, I hope you come out of this with a much better understanding of it. It’s a powerful method and when combined with other methods it will allow you to do a lot of things with your data that form part of a typical front-end work. map()
is an essential tool in any JS dev’s tool belt. So it pays to get very familiar with it and comfortable with its usage. I hope these real-world examples help get you started. But it’s also normal to find a lot of this confusing at first.
As always, the best way to get good at using it is to practice a lot. Open the console and try out some of these examples or even fork the GitHub project of the example and try changing things and mapping to transform other product properties. And if you find yourself stuck or have any questions I’m always happy to lend a hand, you can find me on Twitter for everything dev.