.map()
, .filter()
и .reduce()
, всё, что я читал, смотрел и слушал, казалось мне очень сложным. Эти концепции рассматривались как некие самостоятельные механизмы, ни к чему другому отношения не имеющие. Мне тяжело было ухватить их суть и их понять.
Я слышал, что это — базовые вещи, понимание которых является чем-то вроде границы между «посвящёнными» и «непосвящёнными». Хотелось бы мне тогда, чтобы мне сказали о них правду. Она заключается в том, что эти три метода символизируют то, что причины, по которым перебирают некие итерируемые объекты, часто вписываются в одну из трёх функциональных категорий.
Просматривая код, который я писал раньше, я понял, что в 95% случаев, когда я перебирал элементы строк или массивов, я выполнял одно из следующих действий:
- Применение к каждому значению некоей последовательности инструкций (аналог метода
.map()
). - Фильтрация значений, соответствующих заданному критерию (то же, что делает
.filter()
). - Сведение набора данных к единственному агрегированному значению (аналог
.reduce()
).
Это был момент истины. Именно тогда я понял суть этих методов и увидел их связь с тем, что мне уже давно известно.
Для того чтобы попрактиковаться, я взял свой старый код и отрефакторил его с использованием этих методов. Это оказалось весьма полезным занятием.
А теперь, без лишних слов, давайте поговорим об этих методах, и, в частности, посмотрим на то, как использовать их вместо широко распространённых схем применения циклов.
Метод .map()
Метод
.map()
используется в том случае, если нужно сделать следующее:- Надо выполнить над каждым элементом итерируемого объекта некую последовательность действий.
- Надо вернуть значение, которое, предположительно, было изменено.
Рассмотрим простой пример, в котором для каждого элемента массива, содержащего цены, нужно найти новые суммы, включающие в себя изначальные цены и налог с продаж:
const prices = [19.99, 4.95, 25, 3.50];
let new_prices = [];
for(let i=0; i < prices.length; i++) {
new_prices.push(prices[i] * 1.06);
}
Вот как сделать то же самое с помощью
.map()
:const prices = [19.99, 4.95, 25, 3.50];
let new_prices = prices.map(price => price * 1.06);
Тут используются довольно-таки лаконичные синтаксические конструкции. Поэтому давайте разберём этот пример. Метод
.map()
принимает коллбэк. Это — функция, которая будет применяться к элементам массива. В данном случае это — стрелочная функция, которая объявлена прямо в круглых скобках, следующих за объявлением метода.Имя параметра
price
— это то имя, которое будет использоваться при работе с элементами массива. Так как наша стрелочная функция имеет всего один параметр — мы можем обойтись без круглых скобок при её объявлении.Выражение после стрелки (
=>
) — это тело коллбэка. Так как в теле функции имеется лишь одно выражение — мы можем обойтись без фигурных скобок и без ключевого слова return
.Если такая запись кажется вам непонятной — вот немного расширенный вариант этого примера:
const prices = [19.99, 4.95, 25, 3.50];
let new_prices = prices.map((price) => {
return price * 1.06
});
Метод .filter()
Метод
.filter()
применяется в тех случаях, когда из итерируемого объекта нужно выбрать некие элементы. При использовании этого метода нужно помнить о том, что значения, соответствующие фильтру, включаются в итоговый результат, а не исключаются из него. То есть — всё, для чего функция, переданная .filter()
, возвратит true
, будет оставлено.Рассмотрим пример, в котором нужно отобрать из массива целых чисел только нечётные элементы. Здесь мы воспользуемся оператором взятия остатка от деления и будем выяснять — имеется ли остаток от деления каждого элемента массива на 2. Если остаток равен 1 — это говорит нам о том, что соответствующее число является нечётным. Сначала взглянем на способ решения этой задачи с помощью обычного цикла:
const numbers = [1,2,3,4,5,6,7,8];
let odds = [];
for(let i=0; i < numbers.length; i++) {
if(numbers[i] % 2 == 1) {
odds.push(numbers[i]);
}
}
Метод
.filter()
, как и .map()
, принимает один коллбэк, которому будут поочерёдно передаваться элементы итерируемого объекта:const numbers = [1,2,3,4,5,6,7,8];
let odds = numbers.filter(num => num % 2);
Тут работа организована так же, как и в примере с
.map()
. Стрелочная функция, передаваемая .filter()
, использует лишь один параметр, поэтому мы обходимся без круглых скобок. Её тело содержит лишь одно выражение, поэтому его можно не заключать в фигурные скобки и допустимо обойтись без return
.Метод .reduce()
И вот мы, наконец, добрались до метода
.reduce()
. Он, полагаю, самый непонятный из трёх рассматриваемых сегодня методов. Имя этого метода намекает на то, что он используется для сведения нескольких значений к одному. Однако мне кажется, что легче размышлять о нём как о методе, который позволяет собирать некие значения из частей, а не как о методе, который позволяет что-то «сворачивать» или «редуцировать».При конструировании кода, в котором вызывается этот метод, сначала задают некое начальное значение. По мере того, как метод перебирает значения массива, это начальное значение модифицируется и, в изменённом виде, передаётся в следующую итерацию.
Вот классическая задача, для решения которой нужно вычислить сумму элементов массива. В нашем случае она заключается в поиске суммы пожертвований на некий благотворительный проект:
const donations = [5, 20, 100, 80, 75];
let total = 0;
for(let i=0; i < donations.length; i++) {
total += donations[i];
}
В отличие от методов
.map()
и .filter()
, метод .reduce()
нуждается в коллбэке, принимающем два параметра. Это — аккумулятор и текущее значение. Аккумулятор — это первый параметр. Именно он модифицируется на каждой итерации и передаётся в следующую:const donations = [5, 20, 100, 80, 75];
let total = donations.reduce((total,donation) => {
return total + donation;
});
Методу
.reduce()
тоже можно передать второй аргумент. Это — то, что будет играть роль начального значения для аккумулятора. Предположим, мы хотим узнать общую сумму пожертвований за два дня, учитывая то, что вчера эта сумма составила $450, а сведения о сегодняшних пожертвованиях хранятся в массиве:const donations = [5, 20, 100, 80, 75];
let total = donations.reduce((total,donation) => {
return total + donation;
}, 450);
Итоги
Надеюсь, теперь вы разобрались с методами JS-массивов
.map()
, .filter()
и .reduce()
. Воспринимайте их как механизмы, улучшающие читабельность вашего кода. Они позволяют писать более компактные программы, чем те, которые получаются при использовании обычных циклов. Но самая главная их сильная сторона заключается в том, что они позволяют ясно выразить намерение, которое лежит в основе кода.Благодаря этим методам код, который написан довольно давно, будет легче читать. Вместо того, чтобы вникать в конструкции, помещённые внутрь циклов
for
, делая это лишь для того, чтобы понять их конечную цель, вы, лишь увидев имя одного из этих методов, уже сможете сформировать общее представление о причинах существования того или иного участка кода.Уважаемые читатели! Пользуетесь ли вы методами JS-массивов .map(), .filter() и .reduce()?


Комментарии (25)
RUSich101
20.12.2019 13:06-2Не пользуюсь. Специфика моего JS в том, что он запускается в IE разных версий и в некоторых версиях ещё не поддерживается…
Когда столкнулся со стрелочной функцией в чужом коде, довольно долго втыкал, что это такое… Хоть концепция лямбд мне и была знакома, но в JS она как — то слишком лаконична мне показалась…Carduelis
20.12.2019 13:14IE разных версий и в некоторых версиях ещё не поддерживается…
Используйте babel + browserify, и ваш код будет чистым, современным, поддерживаемым. А если вдруг в вашей компании случится автобусный инцидент, вы будете востребованы на рынке.
vvadzim
20.12.2019 13:16+1Специфика моего JS в том, что он запускается в IE разных версий и в некоторых версиях ещё не поддерживается…
С прискорбием спешу сообщить, что уже и не будет.RUSich101
20.12.2019 13:52Ну это то понятно, что не будет…
IE встраивается в конкретное приложение и на форме приложения выводит интерфейс.
Приложение писано не нами и нет внятных перспектив, что IE поменяют на что то ещё…vvadzim
20.12.2019 14:22Ну так полифилы есть.
Хозяин-барин, конечно, но в сегодняшних реалиях писать код с оглядкой на браузер это либо совсем трудные случаи — canvas vs svg vs vml там, либо особое удовлетворение с этого иметь.
YuriM1983
20.12.2019 14:24Лично я не пользуюсь, потому что в процессе разработки приходится то добавлять, то убирать команды внутри callback-функции. И индекс иногда то нужен, то нет.
Постоянно переписывать for на map и обратно — задалбывает.
А такие простые случаи, что указаны в посте, редко встречаются.JustDont
20.12.2019 14:28+1И индекс иногда то нужен, то нет.
В callbackFn мапа вообще-то есть индекс.
Zenitchik
20.12.2019 15:24то убирать команды внутри callback-функции
В чём проблема? Если неудобно со стрелочной функцией — передайте туда обычную.
maggg
20.12.2019 20:19Вы можете передать индекс в колбэк как второй параметр. В случае
reduce
— как третий.
justboris
20.12.2019 20:18+1Алло, указанные в статье функции поддерживаются начиная с IE9. О какой поддержке вообще идет речь?
latonita
20.12.2019 14:06Есть особенность map (и foreach), которую я не знал и столкнулся с проблемами — нет гарантии, что элементы будут обработаны последовательно. В моем случае просто переделал на стандартный цикл For и все встало на свои места
rampels
20.12.2019 15:15Забыли добавить один нюанс. Если в методе .reduce() явно не задать значение в аккумулятор, то для аккумулятора будет использовано, первое значение переданного массива.
Zenitchik
20.12.2019 15:26-1А у меня обычно в коллбеке ошибка возникала, если я забывал аккумулятор указать.
Valery4
20.12.2019 18:46Возможно имелось в виду, не второй атрибут колбека — он как раз обязателен, а второй опциональный параметр, после колбека, который содержит начальное значение аккумулятора.
В примере сdonations
как раз такой случай и показан.JustDont
20.12.2019 19:17он как раз обязателен
Да сфигали? Ничего там не обязательно вообще.
[1,2,3].reduce(() => { console.log(`woo!`); })
Valery4
20.12.2019 19:22Ну с точки зрения языка — нет.
А с целью суммирования элементов массива — можно пример без него?JustDont
20.12.2019 19:36Ну разумеется можно, потому что там еще два параметра есть:
[1,2,3,4].reduce((acc, _, index, src) => acc + src[index]);
но конечно не рекомендую так где-бы то ни было извращаться ради суммирования.
Zenitchik
20.12.2019 22:33Я понял о чём речь, просто по воле случая, все мои редьюсеры критично зависели от валидной инициализации аккумулятора. Я сейчас напряжённо думаю, как так вышло…
razielvamp
20.12.2019 16:47Если привыкнуть, то выглядит очень удобно. Коротко и элегантно, но для меня все эти JS фишки никогда не будут удобочитаемыми.
Ключевое for сразу подразумевает о перечислении, независимо от языка и примерный смысл можно понять даже если с языком не работал, а в специфические сокращения у JS иногда приходится долго втыкать, если до этого плотно сидел на каком-нибудь pythonZenitchik
20.12.2019 17:02в специфические сокращения у JS иногда приходится долго втыкать, если до этого плотно сидел на каком-нибудь python
Наоборот — тоже.
zoryamba
22.12.2019 13:58map, filter и reduce — базовые функции, которые есть в любом высокоуровневом языке, предоставляющем возможности функционального программирования. и python — не исключение. не понимаю, о каких «специфических сокращениях» идет речь…
подобное можно сказать только о яваскриптовом [].forEach(). тут соглашусь — действительно выглядит как костыль
bp16
Проблема в восприятии новых функций/конструкций кроется в отсутствии ассоциаций с чем-либо уже понятым. Расписали отлично, спасибо!