diff --git a/README.md b/README.md index ea7781a72..9740714b7 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,19 @@ ## Learning Goals -- Learn how the `reduce()` method works -- Demonstrate using `reduce()` with a primitive return value -- Demonstrate using `reduce()` with an object as the return value +- Learn how the `Array.prototype.reduce()` method works +- Demonstrate using `Array.prototype.reduce()` ## Introduction +| Use Case | Method | +| -------------------------------------------------------------- | --------------------- | +| Executing a provided callback on each element | `forEach()` | +| Finding a single element that meets a condition | `find()`, `indexOf()` | +| Finding and returning a list of elements that meet a condition | `filter()` | +| Modifying each element and returning the modified array | `map()` | +| ** Creating a summary or aggregation of values in an array | `reduce()` | + In the world of programming, we often work with lists. Sometimes we want to find or transform elements in a list, but other times we might want to create a single summary value. In this lesson, we'll learn how to use the `reduce()` @@ -53,13 +60,14 @@ iterate through the products in the basket and add the price of each to the total. When the loop has finished, we return the `totalPrice`. This is a very basic way to manually add together the prices of the products we -want to buy, but it only works for this very specific situation. We could make -our solution more abstract by writing a generalized function that accepts two -additional arguments: an initial value and a callback function that implements -the reduce functionality we want. +want to buy, but it only works for this very specific situation: iterating +through an array of objects, each of which has a `price` property, adding +together all the prices, and returning the total. -To see what this might look like, let's count the number of coupons we have -lying around the house: +What if we wanted to calculate something else, say, the number of coupons we +have lying around the house? We can make our solution more abstract by creating +a separate reducer function that implements the reduce functionality we want and +passing that in to `reduce()` as an argument: ```js const couponLocations = [ @@ -69,8 +77,8 @@ const couponLocations = [ { room: "Master bedroom", amount: 7 }, ]; -function ourReduce(arr, reducer, init) { - let accum = init; +function reduce(arr, reducer) { + let accum = 0; for (const element of arr) { accum = reducer(accum, element); } @@ -81,53 +89,138 @@ function couponCounter(totalAmount, location) { return totalAmount + location.amount; } -console.log(ourReduce(couponLocations, couponCounter, 0)); // LOG: 15 +reduce(couponLocations, couponCounter); +// => 15 ``` -`ourReduce()` accepts three arguments: the array we want to reduce, the callback -function or _reducer_, and the initial value for our _accumulator_ variable. -It then iterates over the array, calling the reducer function each time, which -returns the updated value of the accumulator. The final value of the accumulator -is returned at the end. - -Note that `ourReduce()` is generalized: the specifics (the callback function and -initial value) have been abstracted out, making our code more flexible. If, for -example, we already have three coupons in our hand, we can easily account for -that without having to change any code by adjusting the initial value when we -call `ourReduce()`: +Let's walk through what's happening here: + +1. In the last line, we call `reduce()` and pass in the two arguments: the + input array (`couponLocations`) and the reducer function (`couponCounter`). +2. `reduce` first initializes the accumulator variable (`accum`) to 0, then + begins iterating through the input array. +3. Within each iteration, `reduce` calls our reducer function, passing the + current value of the accumulator and the current element as arguments. +4. The reducer function adds together the current value of `accum` and the value + of the `amount` property of the current element of the array and returns that + sum. +5. That returned value becomes the current version of `accum` for the next + iteration. +6. Once `reduce` has iterated through all the elements in the array, it + returns the final value of `accum`, i.e., the total number of coupons. + +But there's still a bit more abstraction we can do. What if we already have +three coupons in our hand? We could change the initial value of the `accum` +variable inside `reducer()`, but that would make our function less +generalized. Instead, we can have `reducer()` take a third argument: the +initial value to use for the accumulator: ```js -console.log(ourReduce(couponLocations, couponCounter, 3)); // LOG: 18 +const couponLocations = [ + { room: "Living room", amount: 5 }, + { room: "Kitchen", amount: 2 }, + { room: "Bathroom", amount: 1 }, + { room: "Master bedroom", amount: 7 }, +]; + +function reduce(arr, reducer, init) { + let accum = init; + for (const element of arr) { + accum = reducer(accum, element); + } + return accum; +} + +function couponCounter(totalAmount, location) { + return totalAmount + location.amount; +} + +reduce(couponLocations, couponCounter, 3); +// => 18 ``` -## Demonstrate using `reduce()` with a Primitive Return Value +Now our accumulator variable is initialized to 3 instead of 0. And if we cut a +couple more coupons out of the newspaper, we can easily calculate the new total +without changing any code. + +To summarize, `reduce()` accepts three arguments: the array we want to +reduce, the callback function or _reducer_, and the initial value for our +_accumulator_ variable. It then iterates over the array, calling the reducer +function each time, which returns the updated value of the accumulator, which in +turn becomes input for the reducer function in the next iteration. The final +value of the accumulator is returned at the end. + +Note that our version of `reduce()` is now fully generalized: the specifics (the +callback function and initial value) have been abstracted out, making our code +more flexible. + +## Demonstrate using `Array.prototype.reduce()` With JavaScript’s native `reduce()` method, we don't need to write our own -version. Just like `ourReduce`, the `reduce()` method is used when we want to -get some information from each element in the collection and gather that -information into a final summary value. Let's take the native implementation for -a spin with our previous example: +version. Just like our `reduce()` function, the `Array.prototype.reduce()` +method is used when we want to get some information from each element in the +collection and gather that information into a final summary value. Let's take +the native implementation for a spin with our previous example: ```js -console.log(couponLocations.reduce(couponCounter, 0)); // also logs 15! +// the input array +const couponLocations = [ + { room: "Living room", amount: 5 }, + { room: "Kitchen", amount: 2 }, + { room: "Bathroom", amount: 1 }, + { room: "Master bedroom", amount: 7 }, +]; + +// the callback function, often referred to as the reducer function +function couponCounter(totalAmount, location) { + return totalAmount + location.amount; +} + +couponLocations.reduce(couponCounter, 0); +// => also logs 15! ``` +Let's walk through this step by step: + +1. At the bottom, we call `reduce()` on our array and pass the callback (or + reducer function) and the start value as arguments. +2. Before starting to iterate through the array, `reduce()` sets the value of + the accumulator variable to the start value that was passed in. +3. Next, `reduce()` calls the reducer function, passing two values: the current + element of the array and the current value of the accumulator variable. +4. The reducer, `couponCounter`, adds the two values together and returns the + total. +5. `reduce()` then calls the reducer function again, passing the next element in + the array and the updated value of the accumulator that was returned by the + reducer in the previous step. +6. This continues until the iteration is complete, and `reduce()` returns the + final value of the accumulator. + +
What is different about calling `.reduce()` vs. any of the other iterator methods? + +
+ Another simple numerical example: ```js -const doubledAndSummed = [1, 2, 3].reduce(function (accumulator, element) { +const nums = [1, 2, 3]; +function doubleAndSum(accumulator, element) { return element * 2 + accumulator; -}, 0); +} + +const doubledAndSummed = nums.reduce(doubleAndSum, 0); // => 12 ``` -Here, as in the previous example, we are calling `.reduce()` on our input array -and passing it two arguments: the callback function, and an optional start value -for the accumulator (0 in this example). `.reduce()` executes the callback for -each element in turn, passing in the current value of the accumulator and the -current element each time. The callback updates the value of the accumulator in -each iteration, and that updated value is then passed as the first argument to -the callback in the next iteration. When there's nothing left to iterate, the +To review: here, as in the previous example, we are calling `.reduce()` on our +input array and passing it two arguments: the reducer and an optional start +value for the accumulator (0 in this example). `.reduce()` executes the callback +for each element in turn, passing in the current value of the accumulator and +the current element each time. The callback updates the value of the accumulator +in each iteration, and that updated value is then passed as the first argument +to the callback in the next iteration. When there's nothing left to iterate, the final value of the accumulator (the total) is returned. The initialization value is optional, but leaving it out might lead to a real @@ -135,111 +228,59 @@ surprise. If no initial value is supplied, the _first element in the array_ is used as the starting value. `reduce()` then executes the callback function, passing this starting value and the _second_ element of the array as the two arguments. In other words, the code inside the callback **is never executed** -for the first element in the array. This can lead to unexpected results: +for the first element in the array. -```js -const doubledAndSummed = [1, 2, 3].reduce(function (accumulator, element) { - return element * 2 + accumulator; -}); -// => 11 -``` - -In some cases, it won't matter (e.g., if our reducer is simply summing the -elements of the input array). However, to be safe, it is best to always pass a -start value when calling `reduce()`. Of course, that initial value can be -anything we like: +This works fine in some cases, for example, if the reducer is simply summing the +values of the input array: ```js -const doubledAndSummedFromTen = [1, 2, 3].reduce(function ( - accumulator, - element -) { - return element * 2 + accumulator; -}, -10); -// => 22 +const nums = [1, 2, 3]; +function sumValues(accumulator, element) { + return element + accumulator; +} + +const summedValues = nums.reduce(sumValues); +// => 6 ``` -## Demonstrate using `reduce()` with an Object as the Return Value +Here, there is no start value, so `reduce()` sets the accumulator equal to the +first element in the array, `1`. It then starts the iteration with the second +element `2`, adds those together to get 3, passes that value to the final +iteration, and finally returns the total, `6`. -The output of the `reduce()` method does not need to be a primitive value like a -`Number` or `String`. Let's consider a couple of examples that accumulates array -values into an `Object`. +> Before moving on, think through what would happen if we passed a start value +> of `0` to `reduce()` in the example above and make sure you understand why the +> same total would be returned. -First, let's look at an example where we take an array of letters and return -an object with letters as keys and their count in the array as values. +However, with more complicated reducers, not passing a start value can lead to +unexpected results. Looking again at our `doubleAndSum` example: ```js -const letters = ["a", "b", "c", "b", "a", "a"]; - -const lettersCount = letters.reduce(function (countObj, currentLetter) { - if (currentLetter in countObj) { - countObj[currentLetter]++; - } else { - countObj[currentLetter] = 1; - } - return countObj; -}, {}); +const nums = [1, 2, 3]; +function doubleAndSum(accumulator, element) { + return element * 2 + accumulator; +} -console.log(lettersCount); // { a: 3, b: 2, c: 1 } +const doubledAndSummed = nums.reduce(doubleAndSum); +// => 11 ``` -We initialize the `countObj` as an empty object by passing `{}` as the second -argument to the `reduce` method. The callback method increments the current -letter's count in the `countObj` if it already exists otherwise, initializes it to -`1`. +Here, because we didn't pass a start value, `reduce()` sets the accumulator +variable equal to the first element in the array and doesn't pass that value +into the reducer. As a result, the first element (`1`) _does not get doubled_ +and the final total is `11` rather than `12`. -Let's look at a more complex example now. Say we want to create a roster of -student artists based on their discipline of art for their final showcase. -Our start value might look like this: +To be safe, it is best to always pass a start value when calling `reduce()`. Of +course, that initial value can be anything we like: ```js -const artsShowcases = { - Dance: [], - Visual: [], - Music: [], - Theater: [], - Writing: [], -}; -``` - -Imagine we also have a `studentSorter` object that includes a `showcaseAssign()` -method. That method takes the name of a student as its argument and returns the -name of the showcase the student should be assigned to. Note that we have not -coded out the `showcaseAssign()` method — the details of how it would work are -not important for our purposes. What's important to remember is that the method -takes the name of a student as an argument and returns one of the five showcases: -"Dance", "Visual", "Music", "Theater" or "Writing". We want to call the method -for each element in our input array (each student's name), get the value of the -showcase that's returned, and add the student's name to the array for that -showcase in the `artsShowcases` object. - -To do that, we will call reduce on our input array, `incomingStudents`, which -contains the names of all incoming students, passing a callback function and the -start value of `artsShowcases` as the arguments. The callback is where we'll -push each student name into the appropriate showcase: - -```js -incomingStudents.reduce(function (showcases, student) { - showcases[studentSorter.showcaseAssign(student)].push(student); -}, artsShowcases); +const doubledAndSummedFromTen = nums.reduce(doubleAndSum, 10); +// => 22 ``` -Let's break this down: `.reduce()` executes the callback for each student name -in turn. Inside the callback, the `studentSorter.showcaseAssign()` method is -called with the current student name as its argument. `showcaseAssign()` returns -the name of an Arts Showcase, which is then used as the key to access the -correct array in the `artsShowcases` object and push the student's name into it. -The iteration then continues to the next element in the array, passing the next -student name and the updated value of `artsShowcases` as the arguments. Once -`reduce()` has iterated through all the students in `incomingStudents`, it -returns the final value of `artsShowcases`. +**REMOVED EXAMPLE WITH OBJECT AS RETURN VALUE - NEEDS TO MOVE LATER** -We can then access the list of students in any Arts Showcase: - -```js -artsShowcases["Visual"]; //=> [yishayGarbasz, wuTsang, mickaleneThomas] -``` +**TAKE OUT LAB PORTION?** ## Lab: Use `reduce()` to Create a Single Aggregate of All Items in a List @@ -265,10 +306,25 @@ up to GitHub, then submit your work to Canvas using CodeGrade. ## Conclusion -With `reduce()`, we are able to quickly get a single summary value from the -elements in an array. `reduce()` — like the other iterator methods we've learned -about in this section — can greatly cut down the amount of time spent recreating -common functionality. It can also make our code more efficient and expressive. +The `reduce()` method can be a bit tricky to wrap your head around at first. +Keeping the following in mind should help: + +1. When you call `reduce()`, you should pass two arguments: the callback (or + reducer) and a start value for the accumulator. Even though the start value + is optional, always including it will help you avoid unexpected results and + it will make your code clearer. +2. `reduce()` will automatically pass two arguments when it calls your reducer + function: the current element of the array and the current value of the + accumulator. Your function needs to accept those two values as parameters, + combine them in some way, and return the result so `reduce()` can use it in + the next iteration. + +The advantage of using `reduce()`, of course, is that it makes it possible to +quickly get a single summary value from the elements in an array without having +to write the looping code ourselves. `reduce()` — like the other iterator +methods we've learned about in this section — can greatly cut down the amount of +time spent recreating common functionality. It can also make our code more +efficient and expressive. ## Resources