Пожаловаться

Упрости свой JavaScript – используй map, reduce и filter

11687
Пожаловаться

Бросай forEach – открывай новые горизонты! Введение в перебирающие методы массивов, которые должен знать каждый JavaScript разработчик.

Упрости свой JavaScript – используй map, reduce и filter

Язык JavaScript оказывает явное предпочтение массивам перед другими структурами данных. У них много удобных специфических фишек, например, целый набор перебирающих методов: forEach, map, filter, reduce.

Но если с первым знакомы практически все программисты, остальные порой остаются в тени. Хватит терпеть эту несправедливость! Пора вывести темных лошадок на свет и как следует в них разобраться.

map

Рассмотрим простой пример. У вас есть массив со множеством объектов, каждый из которых представляет отдельного человека. Тут может быть очень много данных: имя, возраст, цвет волос и любимый персонаж из кинематографа. Но в данный момент всё это не требуется – вы хотите получить только массив паспортных номеров этих людей, чтобы выдать им всем пропуска на конференцию.

// что у вас есть
const friends = [
 { passport: '03005988', name: 'Joseph Francis Tribbiani Jr', age: 32, sex: 'm' },
 { passport: '03005989', name: 'Chandler Muriel Bing', age: 33, sex: 'm' },
 { passport: '03005990', name: 'Ross Eustace Geller', age: 33, sex: 'm' },
 { passport: '03005991', name: 'Rachel Karen Green', age: 31, sex: 'f' },
 { passport: '03005992', name: 'Monica Geller', age: 31, sex: 'f' },
 { passport: '03005993', name: 'Phoebe Buffay', age: 34, sex: 'f' },
]

// что вы хотите получить
[03005988, 03005989, 03005990, 03005991, 03005992, 03005993]

Программирование на JavaScript предлагает множество способов решить эту задачку. Например, можно создать пустой массив, а затем проитерировать любым способом persons и добавлять идентификаторы по одному.

// Так
const passports= [];
for (let i = 0; i < friends.length; i++) {
  passports.push(friends[i].passport);
}

// Или так
const passports = [];
friends.forEach(friend => passports.push(friend.passport));

При этом приходится выполнять лишнюю операцию создания массива. Смотрите, насколько проще всё выглядит с методом map():

const passports = friends.map(function(friend) {
  return friend.passport;
});

// то же самое со стрелочной функцией
const passports = friends.map(friend => friend.passport);

Как это работает?

Метод map принимает функцию-коллбэк, которая будет последовательно вызвана для каждого элемента массива. Она обязана вернуть некоторое значение, которое попадет в результирующий массив.

В нашем случае вот что будет происходить под капотом:

  1. Метод map самостоятельно создаст новый пустой массив, так что вы избавлены от необходимости писать лишнюю инструкцию.
  2. Первое значение исходного массива friends{ passport: '03005988', name: 'Joseph Francis Tribbiani Jr', age: 32, sex: 'm' }. Оно будет передано как аргумент в коллбэк. Коллбэк вернет значение 03005988, которое займет первое место в новом массиве.
  3. Для второго человека { passport: '03005989', name: 'Chandler Muriel Bing', age: 33, sex: 'm' } произойдет то же самое, но в массив попадет уже значение 03005989.
  4. После того, как все элементы исходной коллекции будут обработаны коллбэком, метод map вернет заполненный новыми данными результирующий массив.

В map можно передать еще один аргумент – контекст выполнения. Он будет подставлен как this в коллбэке (если коллбэк вдруг решит обратиться к this).

Полученный массив всегда равен по длине исходному. Даже если обработчик вернет ложное значение или вообще ничего не вернет (в этом случае будет undefined).

reduce

Метод reduce также запускается в контексте массива и вызывает коллбэк для каждого элемента. Но помимо этого, он аккумулирует результаты всех вызовов в одно значение. Этим поведением можно управлять.

Reduce предназначен не для того, чтобы изменять элементы коллекции, как map. Его задача – подсчитать "сумму" всех элементов тем или иным способом, и вернуть ее.

Результирующим значением может быть что угодно: число, строка, объект, массив – все зависит от задачи, которую решает JavaScript разработчик.

Вернемся к нашим друзьям и подсчитаем, сколько им всем лет в сумме. Сделать это можно с помощью цикла for или привычного метода forEach:

// решение с for
let totalYears = 0;
for (let i = 0; i < friends.length; i++) {
  totalYears += friends[i].age;
}

// решение с forEach
let totalYears = 0;
friends.forEach(friend => totalYears += friend.age);

Все просто, но приходится создавать отдельный счетчик и каждый раз его увеличивать. Reduce берет эту непосильную задачу на себя:

let totalYears = friends.reduce(function(accumulator, friend) {
  return accumulator + friend.age;
}, 0);

// то же самое со стрелочной функцией
let totalYears = friends.reduce((accumulator, friend) => accumulator + friend.age, 0);

// Результат: 194

Метод reduce принимает 2 параметра:

  • коллбэк, как и map, который будет вызван последовательно для каждого элемента коллекции;
  • начальное значение аккумулятора.

В коллбэке тоже 2 аргумента:

  • первый – это накопленное значение (аккумулятор);
  • второй – непосредственно элемент массива.

Начальное значение аккумулятора

Разберемся с начальным значением. В примере оно равно 0, так как мы считаем численное значение – сумму возрастов. Это тот же самый 0, который мы помещали в переменную totalYears в примере с forEach, просто здесь он органично вписан в сигнатуру метода.

На месте нуля может быть любое другое число/строка (пустая или нет)/объект/массив – любое значение, с которого вы начинаете аккумуляцию. Для примера объединим имена всех друзей в одну строчку:

let names = friends.reduce((accumulator, friend) => `${accumulator} ${friend.name}, `, "Friends: ");
// Результат: "Friends:  Joseph Francis Tribbiani Jr,  Chandler Muriel Bing, Ross Eustace Geller,  Rachel Karen Green, Monica Geller, Phoebe Buffay, "

Здесь исходным значением послужила строка "Friends:", к которой постепенно добавились имена всех друзей.

Если вы не указываете исходное значение явно, им неявно становится первый элемент массива. В этом случае коллбэк для него уже не вызывается.

Если вы еще не разобрались с алгоритмом работы метода, давайте рассмотрим его пошагово (на примере с суммированием возраста):

friends.reduce((accumulator, friend) => accumulator + friend.age), 0);
  1. Аккумулятор равен 0 (второй аргумент метода reduce).
  2. Вызывается коллбэк для первого элемента. Параметр accumulator равен 0, параметр friend –  { passport: '03005988', name: 'Joseph Francis Tribbiani Jr', age: 32, sex: 'm' }.
  3. Его результатом становится значение 0 + 32 => 32. Теперь аккумулятор равен 32.
  4. Коллбэк вызывается снова, на этот раз его аргументы 32 и { passport: '03005989', name: 'Chandler Muriel Bing', age: 33, sex: 'm' }.
  5. Результат вызова равен 32 + 33 => 65.
  6. И так далее, пока не будут обработаны все элементы коллекции.
  7. В конце метод вернет накопленное значение аккумулятора.

Reduce необязательно использовать прямо "в лоб", последовательно складывая элементы. Он может решать и менее тривиальные задачи. Найдем, к примеру, самого старшего в нашей компании:

let oldestFriend = friends.reduce((oldest, friend) => {
  return (oldest.age) > friend.age ? oldest : friend;
});

Разобрались, как это работает? Мы хотим получить на выходе объект, соответствующий самому старшему члену компании. Исходное значение аккумулятора oldest не указано явно, поэтому вместо него используется первый элемент массива. Запуск коллбэков начнется со второго элемента, а на выходе мы получим:

// {passport: 3005993, name: "Phoebe Buffay", age: 34, sex: 'f'}

filter

А теперь вы хотите отправить дам на шоппинг, а джентльменов – на футбол. Метод filter() словно специально создан для этого! Разделим большую компанию на две поменьше:

let ladies = friends.filter(function(friend) {
  return friend.sex === "f";
});

let gentlemen = friends.filter(function(friend) {
  return friend.sex === "m";
});

// то же самое со стрелочными функциями
let ladies = friends.filter(friend => friend.sex === 'f');
let gentlemen = friends.filter(friend => friend.sex === 'm');

/* Результат:
ladies =>
 [{passport: '3005991', name: "Rachel Karen Green", age: 31, sex: "f"}
  {passport: '3005992', name: "Monica Geller", age: 31, sex: "f"}
  {passport: '3005993', name: "Phoebe Buffay", age: 34, sex: "f"}]
gentlemen =>
 [{passport: '3005988', name: "Joseph Francis Tribbiani Jr", age: 32, sex: "m"}
  {passport: '3005989', name: "Chandler Muriel Bing", age: 33, sex: "m"}
  {passport: '3005990', name: "Ross Eustace Geller", age: 33, sex: "m"}]
*/

У вас не должно возникнуть проблем с пониманием принципа работы этого метода, однако краткое резюме не помешает.

Результатом работы filter всегда является массив. Если коллбэк для элемента возвращает true (или любое "правдивое" значение), этот элемент попадает в результат, иначе – не попадает. Вот и все.

JavaScript методы map, reduce и filter

Комбинируем методы массивов в JavaScript

Программирование на JavaScript поддерживает удобный паттерн чейнинг (chaining) – объединение нескольких функций в одну цепочку с последовательной передачей результата.

Все три разобранных метода вызываются в контексте массива, а два из них еще и возвращают массив. Таким образом, их очень легко объединить.

Например, посчитаем общий возраст всех девочек мальчиков:

let totalBoysYears = friends
  .filter(friend => friend.sex === "m")
  .reduce((accumulator, friend) => accumulator + friend.age, 0); // 98

Или соберем номера паспортов девочек, чтобы купить им билеты на самолет до Лас-Вегаса:

let girlsPassports = friends
  .filter(friend => friend.sex === "f")
  .map(friend => friend.passport); // ['3005991', '3005992', '3005993']

Кстати, то же самое можно сделать при помощи одного лишь метода reduce. Делитесь своими решениями в комментариях.

Почему не forEach?

Зачем нам вот это вот все, если в JavaScript есть старый добрый надежный forEach? Прежде всего затем, что forEach не такой уж надежный, а кроме того более громоздкий.

Просто сравните два варианта. Здесь мы пропускаем каждый элемент исходного массива через функцию handleFriend.

// forEach
let results = [];
friends.forEach(friend => {
  let formatted = handleFriend(friend);
  results.push(formatted);
});

// map
let results = friends.map(handleFriend);

Javascript-код, написанный с помощью map, filter и reduce легче тестировать – меньше манипуляций, меньше всяких beforeEach() и afterEach().

Просто попробуйте заменить привычный forEach на эти методы и вы уже не сможете остановиться.

А вы используете map, filter и reduce?

11687

Комментарии

Рекомендуем