Бросай forEach
– открывай новые горизонты! Введение в перебирающие методы массивов, которые должен знать каждый JavaScript разработчик.
Язык 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
принимает функцию-коллбэк, которая будет последовательно вызвана для каждого элемента массива. Она обязана вернуть некоторое значение, которое попадет в результирующий массив.
В нашем случае вот что будет происходить под капотом:
- Метод
map
самостоятельно создаст новый пустой массив, так что вы избавлены от необходимости писать лишнюю инструкцию. - Первое значение исходного массива
friends
:{ passport: '03005988', name: 'Joseph Francis Tribbiani Jr', age: 32, sex: 'm' }
. Оно будет передано как аргумент в коллбэк. Коллбэк вернет значение03005988
, которое займет первое место в новом массиве. - Для второго человека
{ passport: '03005989', name: 'Chandler Muriel Bing', age: 33, sex: 'm' }
произойдет то же самое, но в массив попадет уже значение03005989
. - После того, как все элементы исходной коллекции будут обработаны коллбэком, метод
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);
- Аккумулятор равен
0
(второй аргумент методаreduce
). - Вызывается коллбэк для первого элемента. Параметр
accumulator
равен0
, параметрfriend
–{ passport: '03005988', name: 'Joseph Francis Tribbiani Jr', age: 32, sex: 'm' }
. - Его результатом становится значение
0 + 32 => 32
. Теперь аккумулятор равен32
. - Коллбэк вызывается снова, на этот раз его аргументы
32
и{ passport: '03005989', name: 'Chandler Muriel Bing', age: 33, sex: 'm' }
. - Результат вызова равен
32 + 33 => 65
. - И так далее, пока не будут обработаны все элементы коллекции.
- В конце метод вернет накопленное значение аккумулятора.
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
Программирование на 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
на эти методы и вы уже не сможете остановиться.
Комментарии