Перейти к содержанию

filter, map и reduce: обработка каталога

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

Исходные данные остаются прежними: четыре объекта courses из предыдущего урока. Оставьте их объявление в начале app.js; листинги ниже добавляются после него и не заменяют сами данные. Мы не будем писать полноценный поиск: здесь учимся преобразованию уже известного небольшого набора.

Отбор записей

Выбранная тема влияет на результат, но не на источник:

const selectedTopic = 'frontend';
const visibleCourses = courses.filter(course =>
  selectedTopic === 'all' || course.topic === selectedTopic);
console.log(visibleCourses.length);
console.log(courses.length);

Ожидаются три и четыре. Метод filter вызывает переданную функцию для записей и сохраняет в новом массиве те, для которых результат считается истинным. Стрелочная функция содержит знакомое правило из урока об условиях. Текущий объект доступен через параметр course.

При выборе publishing результат содержит только Markdown-курс. При all — все четыре записи. Исходная длина остаётся четырьмя в каждом случае. Это и есть нужная обязанность отбора: вычислить видимый набор, сохранив возможность снова показать остальные темы.

Не забывайте возврат при блоке стрелочной функции. Вариант course => { course.topic === selectedTopic; } возвращает undefined, поскольку выражение внутри блока не является автоматическим return. Такая ошибка создаст пустой результат даже при правильных исходных данных. Выражение без блока или явный возврат решают её.

Преобразование каждой записи

map предназначен для получения результата для каждой записи. Применим его к уже отобранным курсам:

const labels = visibleCourses.map(course =>
  `${course.title}: уроков ${course.lessons}`);
console.log(labels.join('\n'));

Для темы frontend ожидаются три подписи, разделённые переносами строк. В отличие от filter, здесь число результатов соответствует числу обработанных элементов. Объекты не исчезают из источника; новый массив содержит строки вместо объектов. Метод join затем соединяет их для одного сообщения.

Не используйте map только ради побочного вызова console.log, игнорируя полученный массив. Название метода обещает преобразование. Для действий без нужного результата подходит цикл или forEach. Такое различие облегчает чтение программы: по названию видно, какие данные должен получить следующий участок.

Существующая функция formatCourse также может применяться внутри обработчика. Например, course => formatCourse(course.title, course.lessons) оставит правило подписи в одном месте. Главное, чтобы вызов действительно возвращал строку, а не только печатал её. Именно поэтому в уроке о функциях мы разделили вычисление и вывод.

Накопление результата

Для суммы нужен один результат вместо массива. Добавьте:

const totalLessons = visibleCourses.reduce(
  (total, course) => total + course.lessons,
  0,
);
console.log(totalLessons);

При выбранном фронтенде ожидается сорок восемь. Параметр total содержит накопленный результат, course — текущую запись. Начальное значение ноль обозначает сумму до первого курса. После каждой итерации возвращённое число становится накопителем следующего шага.

Начальное значение особенно важно для пустого массива. При visibleCourses = [] результат суммы будет нулём. Без начального аргумента reduce берёт первый элемент как накопитель, а для пустой последовательности вызывает ошибку. В нашем случае первым элементом был бы объект курса, что тоже не соответствует числовому смыслу суммы.

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

Механизм возврата и начального значения приведён в справочнике reduce. Отдельные обязанности методов описаны в справочнике filter и справочнике map.

Порядок отображения

Сортировка по количеству требует числового сравнения и осторожности с изменением массива. Добавьте после отбора:

const orderedCourses = visibleCourses.slice();
orderedCourses.sort((a, b) => a.lessons - b.lessons);
console.log(orderedCourses.map(course => course.lessons).join(', '));

Для фронтенда ожидается 12, 16, 20. sort меняет полученный массив, поэтому сначала создана отдельная последовательность через slice. Объекты внутри остаются теми же, но перестановка ссылок не изменяет исходный порядок courses. Различие сортировки и копирования изложено в MDN о sort.

Функция сравнения возвращает отрицательное, положительное число или ноль. Разность количеств задаёт возрастание. Сортировка без такой функции сравнивает строковые представления и может расположить, например, двадцать перед четырьмя. Поэтому нельзя проверить правильность только на текущих двузначных данных.

Для всех тем две карточки имеют двенадцать уроков. Равное количество означает равный приоритет выбранной сортировки; не придумывайте второе правило без необходимости. Если продукту понадобится алфавитный порядок при равенстве, зададим его явно. Пока сохраняем простое понятное действие по одному полю.

Последовательность преобразований

Отбор и сортировка отвечают на разные вопросы. Сначала мы определяем подходящие курсы, затем задаём порядок выбранного набора, затем создаём подписи. Если отсортировать весь источник напрямую и только потом отбирать, исходная последовательность потеряется для следующего показа. Если преобразовать объекты в строки слишком рано, дальнейшая сортировка по lessons потребует разбирать уже готовую подпись.

Каждый промежуточный массив назван по роли: visibleCourses содержит объекты отбора, orderedCourses содержит объекты в нужном порядке, labels содержит строки. Эти названия помогают увидеть, какие операции ещё возможны. Не возвращайте массив строк из функции, которую следующие этапы считают массивом карточек.

Для большого набора несколько проходов могут стать предметом измерения, но для четырёх записей ясность важнее неподтверждённого обещания ускорения. Не объединяйте все операции в один запутанный цикл только потому, что проходов станет меньше. Сначала получите правильную модель и наблюдаемый результат; затем реальные измерения покажут, где нужна оптимизация.

Наконец, map и filter создают новые массивы, но не глубокие копии объектов. Поэтому обработчик отбора должен читать тему, а не одновременно исправлять количество уроков. Даже один такой побочный эффект изменит последующие суммы. Граница «вычислить представление из данных» остаётся той же при выборе любых методов массива.

Для вопроса «есть ли подходящий курс» не обязательно создавать весь массив отбора и считать его длину. Метод some получает условие и возвращает boolean. Для получения первой подходящей записи есть find, результатом которого может быть undefined. Эти обязанности отличаются от показа всех карточек, поэтому их не подставляем вместо filter без изменения цели.

Например, наличие курса публикации и полный список курсов публикации могут сейчас выглядеть одинаково из-за одной записи. При добавлении второй карточки отличие станет видно. Не выбирайте метод по случайному размеру текущих данных; сформулируйте, должен ли результат быть одним курсом, ответом да/нет или последовательностью.

Если функция преобразования возвращает новый объект с дополнительной подписью, исходная модель тоже может сохраняться неизменной. Но добавлять оформленный текст обратно в загружаемую запись без необходимости не стоит: данные и отображение вновь окажутся смешаны. Для нашего каталога строки нужны только при создании DOM, а модель продолжает работать с названием, темой и числом уроков.

Ручное наблюдение должно сравнивать источник и результаты отдельно: четыре исходные записи, три фронтенд-карточки, сумма сорок восемь и возрастающая последовательность количеств. Затем при all ожидается сумма шестидесяти. Дальше упростим чтение нужных полей с помощью деструктуризации, не меняя модель и обязанности операций.

Оглавление курса