The last function of our three is the reduce()
method (say "convolution"), which is used to aggregate data. Aggregation refers to an operation that calculates a value that depends on the entire dataset. Such operations, for example, include finding the average value, the sum of the elements, greater or lesser. This approach was dealt with in the course on arrays.
reduce()
is a bit more complicated than map()
and filter()
but in general retains the general approach with function passing. Let's implement a code that finds the total amount of money a group of people have. Here you can see the aggregation at once, we need to reduce the amount of money of all users to a single value:
const users = [
{ name: 'John', amount: 19 },
{ name: 'Richard', amount: 1 },
{ name: 'Antony', amount: 4 },
{ name: 'Alex', amount: 16 },
];
let sum = 0;
for (const user of users) {
sum += user.amount;
}
console.log(sum); // => 40
The main difference between aggregation and mapping and filtering is that the result of aggregation can be any type of data - both primitive and compound, such as an array. In addition, aggregation often involves initialization with an initial value, which is commonly referred to as an accumulator. In the example above, it is executed on the line let sum = 0
. Here the variable sum
"accumulates" the result within itself.
Let's look at another example of aggregation - grouping user names by age:
const users = [
{ name: 'Peter', age: 4 },
{ name: 'John', age: 19 },
{ name: 'Antony', age: 4 },
{ name: 'Alex', age: 16 },
];
const usersByAge = {};
for (const { age, name } of users) {
// Check if the age property has already been added to the resulting object or not
if (!Object.hasOwn(usersByAge, age)) {
usersByAge[age] = [];
}
usersByAge[age].push(name);
}
console.log(usersByAge);
// => { 4: [ 'Peter', 'Antony' ], 16: [ 'Alex' ], 19: [ 'John' ] }
In this example, the result of the aggregation is an object that has arrays written in its properties. This result is initially initiated by an empty object, and then gradually, at each iteration, is "filled" with the desired data. The value that accumulates the result of aggregation is commonly referred to as the word "accumulator". In the examples above, these are sum
and usersByAge
.
Let's implement the first example using reduce()
:
const users = [
{ name: 'John', amount: 19 },
{ name: 'Richard', amount: 1 },
{ name: 'Antony', amount: 4 },
{ name: 'Alex', amount: 16 },
];
const sum = users.reduce((acc, user) => {
const newAcc = acc + user.amount;
return newAcc;
}, 0);
// const sum = users.reduce((acc, user) => acc + user.amount, 0);
// Let's describe
// user: John, acc = 0, return value 0 + 19
// user: Richard, acc = 19, return value 19 + 1
// user: Antony, acc = 20, return value 20 + 4
// user: Alex, acc = 24, return value 24 + 16
console.log(sum); // => 40
The reduce()
method takes two parameters as input, the handler function and the initial accumulator value. The same accumulator returns to the outside as the result of the entire operation.
The function passed to reduce()
is the most important part and the key to understanding how the whole aggregation mechanism works. It takes two values as input. The first is the current accumulator value, the second is the current item being processed. The task of the function is to return the new accumulator value. reduce()
does not analyze the contents of the accumulator in any way. All it does is pass it to each new call until the entire collection has been processed, and eventually return it to the outside. I emphasize that you should always return the accumulator, even if it has not changed.
The second example using reduce()
looks like this:
// let's prepare the handler function
const cb = (acc, user) => {
if (!Object.hasOwn(acc, user.age)) {
acc[user.age] = [];
}
acc[user.age].push(user.name);
return acc; // be sure to return it!
};
// Initial value is an empty object
const usersByAge = users.reduce(cb, {});
The code is pretty much the same, except the loop is gone and there is an accumulator return from the anonymous function.
reduce()
Technically, you can work using only it, since it can replace both mapping and filtering. But you shouldn't do that. Aggregation controls the state (accumulator) explicitly. Such code is always more complicated and requires more action. So if it is possible to solve the problem by mapping()
or filtering()
, then this is what you should do.
How to think about reduce
Let's describe an algorithm that will help you correctly approach tasks that require reduce. Imagine that you have a list of courses with lessons within them and you need to count the number of all lessons. For example, this may be necessary to calculate the duration of the training program. Such problems occur regularly on Hexlet.
// A simplified structure so as not to overload
// In reality, there would be a lot of additional data about the course and the lessons
const courses = [
{
name: 'Arrays',
lessons: [{ name: 'One' }, { name: 'Two' } ]
},
{
name: 'Objects',
lessons: [{ name: 'Lala' }, { name: 'One' }, { name: 'Two' } ]
}
];
Here we see two courses with a total of 5 lessons. Now let's try to calculate this number programmatically. The first question to answer is, is this operation an aggregation? The answer is "Yes" because we are reducing the raw data to some calculable result. Then we see what operation results to. In our case, it is a number that is calculated as the sum of the lessons in each course. So the initial value of the accumulator will be 0 (you can refresh it here). Now a rough algorithm:
- Initialize the accumulated result with zero
- Going around the course collection one by one
- Add the number of lessons in the current course to the accumulator
This algorithm will be identical in any variant of the solution, either through a loop or through a redirect:
// loop
let result = 0;
for (const course of courses) {
result += course.lessons.length;
}
console.log(result); // => 5
// reduce
const result = courses.reduce((acc, course) => acc + course.lessons.length, 0);
console.log(result); // => 5
Implementation
Let's write our own myReduce()
, function that works similarly to the reduce()
array method:
const myReduce = (collection, callback, init) => {
let acc = init; // Initializing acc
for (const item of collection) {
acc = callback(acc, item); // Replacing the old acc with a new one
}
return acc;
};
const users = [
{ name: 'Peter', age: 4 },
{ name: 'John', age: 19 },
{ name: 'Antony', age: 4 },
{ name: 'Alex', age: 16 },
];
const oldest = myReduce(
users,
(acc, user) => (user.age > acc.age ? user : acc),
users[0],
);
console.log(oldest); // => { name: 'John', age: 19 }
Recommended materials
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.