Функциональное программирование в JS: map, filter, reduce (ч.5)
Nuances of programmingПеревод статьи Omer Goldberg: Functional Programming in JS: map, filter, reduce (Pt. 5)
Предыдущие статьи: Часть 1, Часть 2, Часть 3, Часть 4
Давайте перейдем сразу к практике! До этого мы изучали функции высшего порядка. Для тех, кто подзабыл, – это функции, параметром которых может выступать другая функция.
В массивах Javascript существует несколько встроенных методов, представляющих собой функции высшего порядка.
В данной статье мы рассмотрим 3 самых популярных метода: filter, map и reduce.
filter
Метод filter (Array) создает новый массив со всеми элементами, соответствующими требованиям данной функции.
Формулировка взята из учебника. Если говорить простым языком, то filter – это метод, выполняемый в данной коллекции/массиве. А элемент фильтра основан на функции, которая возвращает логическое значение (true или false).
Давайте перейдем к примеру и посмотрим, что там происходит. В качестве примера возьмем следующую коллекцию:
const iceCreams = [ { flavor: 'pineapple', color: 'white' }, { flavor: 'strawberry', color: 'red' }, { flavor: 'watermelon', color: 'red' }, { flavor: 'kiwi', color: 'green' }, { flavor: 'mango', color: 'yellow' }, { flavor: 'pear', color: 'green' } ];
Воспользуемся методом filter для создания нового массива только с мороженым красного цвета. Запомните: фильтр создает новый массив, поэтому необходимо сохранять выход функции в переменную для последующего доступа к ней через консоль.
const favoriteFlavors = iceCreams .filter(iceCream => iceCream.color === 'red'); console.log(favoriteFlavors);
Запуск этой части кода в консоли выдаст следующий результат:
Запутались? Это совершенно нормально. Давайте вместе во всем разберемся :)
Метод .filter принимает функцию с 4 аргументами, расположенными в следующем порядке:
· element – текущий элемент массива;
· index – текущий индекс массива (необязательное значение);
· array – ссылка на сам массив (необязательное значение);
· thisArg – используемое значение при выполнении обратного вызова (необязательное значение).
Итого, функция представляется со следующей сигнатурой:
Обратите внимание, что в нашем примере передавалась анонимная функция (не именованная!). Именованная передавалась бы вот так:
const getRed = icecream => icecream.color === 'красный'; const favoriteFlavors = iceCreams .filter(getRed); console.log(favoriteFlavors);
Вывод такой функции:
Учтите, что мы передавали функцию getRed с неявным вызовом параметром элемента.
filter отлично подходит для быстрого разбора данных, оставляя только то, что нам действительно нужно. На первый взгляд, идея передачи функций с определенной структурой может показаться достаточно странной, но со временем вы разглядите в ней довольно мощный инструмент для реализации поставленных задач. Во-первых, такая структура облегчает процесс чтения как своего, так и чужого кода. Во-вторых, мы можем использовать этот шаблон уже сейчас, при разборе метода .map (Array). 😎😎😎😎😎
map
Метод map() создает новый массив с результатами вызова представленной функции по каждому элементу вызываемого массива. Суть в том, что этот метод берет исходный массив и на основании него создает новый. Краткая сигнатура map():
Оба метода – и filter(), и map() – это функции высшего порядка. В map () мы тоже передаем функцию. Но вместо сортировки исходного массива, мы выполняем преобразование данных.
Давайте разберем все на примере! Нам потребуется массив из предыдущего примера.
const iceCreams = [ { flavor: 'pineapple', color: 'white' }, { flavor: 'strawberry', color: 'red' }, { flavor: 'watermelon', color: 'red' }, { flavor: 'kiwi', color: 'green' }, { flavor: 'mango', color: 'yellow' }, { flavor: 'pear', color: 'green' } ];
Предположим, нам нужно создать новый массив строк – в них задан вкус мороженого. До того, как обратиться к map(), решим задачку по старинке – классическим циклом.
let flavors = []; for (let i = 0; i < iceCreams.length; i++) { flavors.push(iceCreams[i].flavor) } console.log(flavors);
С технической точки зрения такие простые примеры реализуются неплохо. Но знаете ли вы о потенциальных проблемах, которые могут возникать при употреблении for в цикле? С моим пессимизмом я вижу сразу 3 недочета 😳😳😳
· Определение значения итератора
let i = 0;
· Определение конечного значения для цикла for
i < iceCreams.length
· Увеличение итератора
i++
При разных сценариях и данных тут можно допустить массу ошибок. Например, пропустить какую-то букву, забыть про точку с запятой или по ошибке задать итератора с некорректным значением.
Теперь давайте попробуем сделать тоже самое, но с методом map().
const iceCreams = [ { flavor: 'pineapple', color: 'white' }, { flavor: 'strawberry', color: 'red' }, { flavor: 'watermelon', color: 'red' }, { flavor: 'kiwi', color: 'green' }, { flavor: 'mango', color: 'yellow' }, { flavor: 'pear', color: 'green' } ]; const flavors = iceCreams.map(icecream => icecream.flavor) console.log(flavors)
Результат такой же, но обратите внимание, какой чистый и лаконичный получился код. Никаких индексов, точек с запятой, объявления длины данных! Начав программировать с map() и filter(), я заметил, что плюсы от использования этих методов продолжают расти, чего не скажешь о циклах, основанных на сложности коллекции / массива. map () и filter () гарантированно стоит внедрять в свою каждодневную работу.
reduce
Наконец-то! Как метко выразился Кристиан Сакай в своем комментарии, reduce – это прародитель всех наших методов
Официльное определение гласит:
Метод reduce() применяет функцию к аккумулятору и каждому элементу массива (слева направо), сводя их к одному значению.
Звучит как-то малопонятно! Попробуем упростить формулировку для большего понимания. Давайте вернемся к методам filter() и map(). Что у них общего? Все они преобразуют одну коллекцию / массив в другую. А разница в том, как именно происходит преобразование данных. Образно говоря, reduce() – швейцарский армейский нож в любом преобразовании списка. Он используется для любого преобразования! По сути, мы можем применять reduce() для реализации map() и filter(). Хватит разговоров! Перейдем к классическому примеру работы с сокращением массива 🙃
Первый пример с циклом for
const arr = [10,20,30] let total = 0; for(let i = 0; i < arr.length; i++) { total += arr[i] } console.log(total);
А теперь с reduce
const arr = [10, 20, 30]; const reducerFunction = (acc, currentItem) => acc + currentItem; const sum = arr.reduce(reducerFunction, 0); console.log(sum);
Само собой, результат у них одинаковый. Но разберем все в деталях. Встроенный метод массива reduce() требует для своего первого параметра функцию обратного вызова. Эта функция имеет заранее определенное входное значение, принимает 4 аргумента, похожих на обратные вызовы из filter() и map(). Рассмотрим ожидаемую сигнатуру функции reducer().
Второй параметр в reduce() не обязателен. Им является initialValue. В случаях, когда мы перебираем элементы массива и пытаемся сократить его до одного значения, рекомендуют конкретизировать начальное значение. В нашем примере суммирования массивов мы определяли initialValue равным нулю. А что бы произошло, если бы мы не стали конкретизировать начальное значение?
const arr = [10, 20, 30]; const reducerFunction = (acc, currentItem) => acc + currentItem; // Not instantiating the initial value! const sum = arr.reduce(reducerFunction); console.log(sum);
Мы получим то же значение! Почему так? Спецификация метода говорит о том, что в случаях, когда не задано значение initialValue, используется первый элемент массива. Несмотря на то, что наш пример выполнился и без установленного начального значения, я настоятельно рекомендую приучать себя к тому, что задавать начальное значение – нужно! Такой подход поможет предотвратить возможные ошибки и заставит вас лишний раз призадуматься о целесообразности сокращения вашего массива.
Реализация map() и filter() с reduce()
Ранее я говорил о том, что reduce() является прародителем методов преобразования списков, потому как его можно использовать для реализации их всех. Давайте докажем это на практике!
// ************* Map с Reduce ************* const data = [10, 20, 30]; const tripledWithMap = data.map(item => { return item * 3; }); const tripledWithReduce = data.reduce((acc, value) => { acc.push(value * 3); return acc; }, []); console.log(tripledWithMap, tripledWithReduce);
filter с reduce()
// ************* Сортировка с сокращением ************* const data2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const evenWithFilter = data2.filter(item => { return item % 2 === 0; }) const evenWithReduce = data2.reduce((acc, value) => { if (value % 2 === 0) { acc.push(value); } return acc; }, []); console.log(evenWithFilter, evenWithReduce);
Более практичный пример с reduce()
Самое время придумать более полезный пример с методом reduce(). Можно рассчитать результаты голосования за лучший вкус мороженого 🍦🍦🍦🍦🍓🍋 🍌 🍉 🍇
const flavours = [ "strawberry", "strawberry", "kiwi", "kiwi", "kiwi", "strawberry", "mango", "kiwi", "banana" ]; const votes = {}; const reducer = (votes, vote) => { votes[vote] = !votes[vote] ? (votes[vote] = 1) : votes[vote] + 1; return votes; }; const outcome = flavours.reduce(reducer, votes); // Output console.log("Strawberry: ", outcome.strawberry); console.log("Kiwi: ", outcome.kiwi); console.log("Mango: ", outcome.mango); console.log("Banana: ", outcome.banana);
Каждый раз при операции сокращения необходимо вызвать в массиве метод reduce(),а также обозначить обратный вызов и initialValue. Обратите внимание, что в данном примере мы задаем изначальное значение в виде пустого объекта. Без такого определения ничего не заработает!
Сглаживание данных с reduce
Для начала давайте определим сглаживаемые данные. Сглаживание выглядит так:
[[a, b, c], [d, e, f], [g, h i]] -> [a, b, c, d, e, f, g, h, i]
То есть мы хотим объединить все массивы по порядку их отображения. reduce() элегантно справляется с решением этой проблемы 🤗🤗
const letterArr = [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]; const flattened = letterArr.reduce((acc, val) => { return acc.concat(val); }, []); console.log(flattened);
Производительность преобразователей списков
Создание цепочки преобразователей списка – явление довольно частое. Несмотря на то, что прочесть сложные преобразования будет легче, этот метод явно проиграет по скорости, особенно при работе с очень большими массивами. Взгляните на пример.
let bigData = []; for (let i = 0; i < 1000000; i++) { bigData[i] = i; } // Slow let filterBegin = Date.now(); const filterMappedBigData = bigData .filter(value => value % 2 === 0) .map(value => value * 2); let filterEnd = Date.now(); let filtertimeSpent = (filterEnd - filterBegin) / 1000 + "secs"; // Fast let reducedBegin = Date.now(); const reducedBigData = bigData.reduce((acc, value) => { if (value % 2 === 0) { acc.push(value * 2); } return acc; }, []); let reducedEnd = Date.now(); let reducedtimeSpent = (reducedEnd - reducedBegin) / 1000 + " secs"; console.log("filtered Big Data:", filtertimeSpent); console.log("reduced Big Data:", reducedtimeSpent);
очему цепочка filter и map такая медленная? Сначала filter() должен выполнить итерацию всего массива (1,000,000) и отфильтровать половину. Затем уже map() проходит по оставшейся части массива (500,000) и создает новый массив. Для сравнения: в reduce() мы перебираем массив только раз! Отсутствие постоянной итерации одинакового набора данных более результативно в плане качества, но менее эффективно по части производительности.
Перевод подготовила Ольга Сайфудинова