A Map From List to ListOften, we find ourselves needing to take an array and modify every element in it in exactly the same way. Typical examples of this are squaring every element in an array of numbers, retrieving the name from a list of users, or running a regex against an array of strings. Show
The syntax for let newArray = arr.map(callback(currentValue[, index[, array]]) { // return element for newArray, after executing something }[, thisArg]); When you call Under the hood,
Let's look at some code. map in PracticeSuppose we have an app that maintains an array of your tasks for the day. Each // Durations are in minutes const tasks = [ { 'name' : 'Write for Envato Tuts+', 'duration' : 120 }, { 'name' : 'Work out', 'duration' : 60 }, { 'name' : 'Procrastinate on Duolingo', 'duration' : 240 } ]; Let's say we want to create a new array with just the name of each task, so we can take a look at everything we've done today. Using a const task_names = []; for (let i = 0, max = tasks.length; i < max; i += 1) { task_names.push(tasks[i].name); } console.log(task_names) // [ 'Write for Envato Tuts+', 'Work out', 'Procrastinate on Duolingo' ] JavaScript also offers a const task_names = []; tasks.forEach(function (task) { task_names.push(task.name); }); console.log(task_names) // [ 'Write for Envato Tuts+', 'Work out', 'Procrastinate on Duolingo' ] Using const task_names = tasks.map(function (task, index, array) { return task.name; }); console.log(task_names) // [ 'Write for Envato Tuts+', 'Work out', 'Procrastinate on Duolingo' ] Here I included the An
even more succinct way of writing const task_names = tasks.map(task => task.name) console.log(task_names) // ['Write for Envato Tuts+', 'Work out', 'Procrastinate on DuoLingo'] Arrow functions are a short form for one-line functions that just have a There are a few important differences between the different approaches:
Turns out, all of the functions we'll look at today share these characteristics. The fact that we don't have to manually manage the state of the loop makes our code simpler and more maintainable. The fact that we can operate directly on the element instead of having to index into the array makes things more readable. Using a
Keeping the number of places where you modify state to an absolute minimum is an important tenet of functional programming. It makes for safer and more intelligible code. GotchasThe callback you pass to If you do forget, Fortunately, this is the only gotcha with ImplementationReading implementations is an important part of understanding. So let's write our own lightweight let map = function (array, callback) { const new_array = []; array.forEach(function (element, index, array) { new_array.push(callback(element)); }); return new_array; }; This code accepts an array and a callback function as arguments. It then creates a new array, executes the callback on each element on the array we passed in, pushes the results into the new array, and returns the new array. If you run this in your console, you'll get the same result as before. While we're using a for loop under the hood, wrapping it up into a function hides the details and lets us work with the abstraction instead. That makes our code more declarative—it says what to do, not how to do it. You'll appreciate how much more readable, maintainable, and, erm, debuggable this can make your code. Filter Out the NoiseThe next of our array operations is The syntax for filter is: let newArray = arr.filter(callback(currentValue[, index[, array]]) { // return element for newArray, if true }[, thisArg]); Just like
Consider the following example, which filters out any string which is less than 8 characters. const words = ['Python', 'Javascript', 'Go', 'Java', 'PHP', 'Ruby']; const result = words.filter(word => word.length < 8); console.log(result); The expected result will be: [ 'Python', 'Go', 'Java', 'PHP', 'Ruby' ] Let's revisit our task example. Instead of pulling out the names of each task, let's say I want to get a list of just the tasks that took me two hours or more to get done. Using const difficult_tasks = []; tasks.forEach(function (task) { if (task.duration >= 120) { difficult_tasks.push(task); } }); console.log(difficult_tasks) // [{ name: 'Write for Envato Tuts+', duration: 120 }, // { name: 'Procrastinate on Duolingo', duration: 240 } // ] With const difficult_tasks = tasks.filter((task) => task.duration >= 120 ); Just like
GotchasThe
callback you pass to If you forget your return statement, your callback will return If
you go the other route and return something that's isn't explicitly Always make sure your
callbacks include an explicit return statement. And always make sure your callbacks in ImplementationOnce again, the best way to understand a piece of code is... well, to write it. Let's roll our own lightweight const filter = function (array, callback) { const filtered_array = []; array.forEach(function (element, index, array) { if (callback(element, index, array)) { filtered_array.push(element); } }); return filtered_array; }; The Reduce MethodThe syntax for the let newArray = arr.filter(callback(currentValue, accumulatedValue) { // return the accumulated value, given the current and previous accumulated value }, initialValue[, thisArg]);
Just like
Notice that the callback gets a previous value on each iteration. On the first iteration, there is no previous value. This is why you have the option to pass Finally, bear in mind that reduce in PracticeLet's say you want to find the sum of a list of numbers. Using a loop, it would look like this: let numbers = [1, 2, 3, 4, 5], total = 0; numbers.forEach(function (number) { total += number; }); console.log(total); // 15 While this isn't a bad use case for const total = [1, 2, 3, 4, 5].reduce(function (previous, current) { return previous + current; }, 0); console.log(total); // 15 First, we call With arrow functions, we would write it like this: const total = [1, 2, 3, 4, 5].reduce((previous, current) => previous+current),0; console.log(total) // 15 If we take it step by step, it looks like this:
If you're not a fan of tables, run this snippet in the console: const total = [1, 2, 3, 4, 5].reduce(function (previous, current, index) { const val = previous + current; console.log("The previous value is " + previous + "; the current value is " + current + ", and the current iteration is " + (index + 1)); return val; }, 0); console.log("The loop is done, and the final value is " + total + "."); To recap: Let's turn back to our tasks example. We've gotten a list of task names from What if we wanted to know the total amount of time we spent working today? Using a let total_time = 0; tasks.forEach(function (task) { // The plus sign just coerces // task.duration from a String to a Number total_time += (+task.duration); }); console.log(total_time) //expected result is 420 With total_time = tasks.reduce((previous, current) => previous + current.duration, 0); console.log(total_time); //420 That's almost all there is to it. Almost, because JavaScript provides us with one more little-known method,
called let array_of_arrays = [[1, 2], [3, 4], [5, 6]]; const concatenated = array_of_arrays.reduce( function (previous, current) { return previous.concat(current); }); console.log(concatenated); // [1, 2, 3, 4, 5, 6];
let array_of_arrays = [[1, 2], [3, 4], [5, 6]]; const concatenated = array_of_arrays.reduceRight( function (previous, current) { return previous.concat(current); }); console.log(concatenated); // [5, 6, 3, 4, 1, 2]; I use GotchasThe three big gotchas with
Fortunately, the first two are easy to avoid. Deciding what your initial value should be depends on what you're doing, but you'll get the hang of it quickly. The last one might seem a bit strange. If There are a few good reasons for that. First, Second, if ImplementationTime for our last look under the hood. As usual, Mozilla has a bulletproof polyfill for reduce if you want to check it out. let reduce = function (array, callback, initial) { let accumulator = initial || 0; array.forEach(function (element) { accumulator = callback(accumulator, array[i]); }); return accumulator; }; Two things to note here:
Putting It Together: Map, Filter, Reduce, and ChainabilityAt this point, you might not be that impressed. Fair enough: Let's say I want to do the following:
First, let's define our tasks for Monday and Tuesday: const monday = [ { 'name' : 'Write a tutorial', 'duration' : 180 }, { 'name' : 'Some web development', 'duration' : 120 } ]; const tuesday = [ { 'name' : 'Keep writing that tutorial', 'duration' : 240 }, { 'name' : 'Some more web development', 'duration' : 180 }, { 'name' : 'A whole lot of nothing', 'duration' : 240 } ]; const tasks = [monday, tuesday]; And now, our lovely-looking transformation: const result = tasks // Concatenate our 2D array into a single list .reduce((acc, current) => acc.concat(current)) // Extract the task duration, and convert minutes to hours .map((task) => task.duration / 60) // Filter out any task that took less than two hours .filter((duration) => duration >= 2) // Multiply each tasks' duration by our hourly rate .map((duration) => duration * 25) // Combine the sums into a single dollar amount .reduce((acc, current) => [(+acc) + (+current)]) // Convert to a "pretty-printed" dollar amount .map((amount) => '$' + amount.toFixed(2)) // Pull out the only element of the array we got from map .reduce((formatted_amount) =>formatted_amount); If you've made it this far, this should be pretty straightforward. There are two bits of weirdness to explain, though. First, on line 10, I have to write: // Remainder omitted reduce(function (accumulator, current) { return [(+accumulator) + (+current_]; }) Two things to explain here:
The second bit that might make you a bit uncomfortable is the last // Remainder omitted map(function (dollar_amount) { return '$' + dollar_amount.toFixed(2); }).reduce(function (formatted_dollar_amount) { return formatted_dollar_amount; }); That call to Finally, let's see how our friend the let concatenated = monday.concat(tuesday), fees = [], hourly_rate = 25, total_fee = 0; concatenated.forEach(function (task) { let duration = task.duration / 60; if (duration >= 2) { fees.push(duration * hourly_rate); } }); fees.forEach(function (fee) { total_fee += fee; }); console.log(total_fee); //400 Tolerable, but noisy. Conclusion and Next StepsIn this tutorial, you've learned how By now, I'm sure you're eager for practice and further reading. For a masterclass in functional programming in JavaScript, check out our online course. Questions, comments, or confusions? Leave them in the comment section. |