Методы массивов: map, filter, reduce

коллекции≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Массив редко нужен «как есть»: чаще его надо перебрать, отсеять лишнее или свернуть в одно число. Для этих трёх задач в JavaScript есть map, filter и reduce — и, разобравшись с ними, вы почти перестанете писать циклы руками.

Разделы темы:·

map — это конвейер. Вы описываете функцию «что сделать с одним элементом», а map прогоняет её по всем элементам подряд и складывает результаты в новый массив. Длина результата всегда равна длине исходного массива: один вошёл — один вышел.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Чаще всего map достают, когда из массива объектов нужен массив чего-то одного: имён, id, цен. Главное правило — функция обязана вернуть значение. Если она ничего не возвращает, на этом месте в новом массиве окажется undefined.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Стрелка без фигурных скобок возвращает значение сама. А как только вы пишете фигурные скобки — return нужен явно, иначе получите массив из undefined.

Функция внутри map, filter и forEach получает не только элемент, но и его индекс. Второй параметр можно просто не писать, если он не нужен.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

filter не меняет значения — он решает, кого пропустить дальше. Ваша функция играет роль охранника на входе: вернула true — элемент попадает в новый массив, вернула false — остаётся за бортом.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Длина результата — от нуля до длины исходного массива, порядок элементов сохраняется. И обратите внимание на разницу: у map внутри функции обычно выражение-преобразование, у filter — условие. Если вы написали в filter что-то вроде n * 2, он не преобразует числа, а просто отсеет нули: любое ненулевое число для него истинно.

reduce сворачивает весь массив в одно значение: сумму, максимум, строку, объект — что угодно. У его функции два главных параметра: аккумулятор (то, что накопилось к этому шагу) и текущий элемент. Что функция вернёт — то и станет аккумулятором на следующем шаге.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Второй аргумент reduce — стартовое значение аккумулятора: 0 для суммы, {} для группировки, [] для сборки массива. Пишите его всегда, даже когда кажется, что можно без него.

Свернуть можно не только в число. Классический приём — группировка: аккумулятор-объект, ключи которого мы дописываем на ходу. Здесь важно не забыть return acc в конце — иначе на следующем шаге аккумулятором станет undefined.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Без второго аргумента reduce берёт первым аккумулятором первый элемент массива и начинает со второго. Обычно это работает — ровно до того дня, когда массив окажется пустым: тогда вызов упадёт с TypeError. Со стартовым значением пустой массив честно вернёт его.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

map и filter возвращают массив, а у массива снова есть map и filter. Поэтому вызовы выстраиваются в цепочку и читаются сверху вниз, как список шагов: «взять чётные, возвести в квадрат, сложить». reduce обычно ставят последним — он возвращает уже не массив.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Порядок в цепочке — не только про смысл, но и про работу впустую: сначала filter, потом map, чтобы преобразовывать меньше элементов. И если цепочка выросла до пяти звеньев, спокойно разбейте её на две переменные с говорящими именами — это не «менее красиво», это понятнее.

map, filter и reduce не трогают исходный массив — они возвращают новое значение. Отсюда самая частая ошибка новичка: вызвать метод и не присвоить результат. Массив останется прежним, а работа пропадёт.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

А вот sort, reverse, push, splice — из другого семейства: они перестраивают сам массив. Поэтому sort в середине цепочки способен незаметно испортить данные, на которые ссылается кто-то ещё. Современная замена — toSorted и toReversed: те же операции, но с копией. В React это различие станет правилом: массив, изменённый на месте, не перерисует экран. Почему так происходит, объясняет статья «Объекты и массивы в состоянии» в части IV.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

forEach — тоже перебор, но он ничего не собирает и всегда возвращает undefined. Его дело — побочное действие: вывести в консоль, отправить запрос, показать alert. Если вы пытаетесь получить из forEach результат, вам был нужен map.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

МетодЧто возвращаетКогда брать
mapновый массив той же длинынужно преобразовать каждый элемент
filterновый массив, короче или той же длинынужно отобрать элементы по условию
reduceодно значение любого типанужно свернуть массив в сумму, объект, строку
forEachundefinedнужно просто сделать что-то с каждым

  • map возвращает новый массив той же длины: одна функция-преобразование на каждый элемент.
  • filter возвращает новый массив только из тех элементов, на которых условие дало true.
  • reduce сворачивает массив в одно значение — сумму, объект, строку; стартовое значение аккумулятора пишите явно.
  • Эти методы не меняют исходный массив: если результат не присвоить, он потеряется. А sort, reverse и push, наоборот, меняют.
  • forEach возвращает undefined и нужен только ради побочного действия; за результатом идите к map, filter или reduce.

1/3Что выведет console.log([1, 2, 3].forEach((n) => n * 2))?

2/3const a = [5, 8, 1]; a.filter((n) => n > 4); console.log(a.length); — что в консоли?

3/3Чему равно [1, 2, 3, 4].reduce((acc, n) => acc + n, 10)?

Стоимость корзины

В массиве items лежат товары: у каждого есть цена и количество. Посчитайте общую стоимость корзины одним вызовом reduce и выведите её.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Аккумулятор здесь — накопленная сумма, стартует с 0. На каждом шаге прибавляем стоимость одной позиции: цену, умноженную на количество.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Через map получилось бы в два шага: сначала items.map((item) => item.price * item.count) даст массив стоимостей, потом reduce сложит их. Оба варианта верные — второй чуть длиннее, зато нагляднее.

Длинные слова заглавными

Из массива слов оставьте только те, где больше четырёх букв, и приведите их к верхнему регистру. Исходный массив трогать нельзя — соберите новый цепочкой методов.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Сначала отбор, потом преобразование: filter оставляет подходящие слова, map меняет каждое из них.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Слово «стул» длиной ровно 4 не проходит: условие строгое, > 4, а не >= 4.