eFusion 25 ноября 2020

☕ map(), filter() и reduce() в JavaScript

Код на JavaScript можно сделать более функциональным, попутно облегчив труд программиста. Знакомимся с методами массивов map(), filter() и reduce().

Перевод публикуется с сокращениями, автор оригинальной статьи Aditya Dhanraj Tiwari.

Методы массивов map, filter и reduce – по сути, лишь некоторые из наиболее известных и простых в использовании функций высшего порядка, которые обеспечивают обратный вызов для каждого элемента. В этой статье мы рассмотрим, как применение map(), filter() и reduce() поможет сделать код:

  • понятным;
  • менее подверженным побочным эффектам, т. к. эти методы не изменяют массив, а создают новый;
  • избегающим явных циклов.

Приступим к рассмотрению.

Примечание переводчика
Обратный вызов позволяет в функции исполнять заданный в аргументе код, который может быть недоступен для прямого вызова. Часто такой подход используется для реализации алгоритма обхода некого хранилища объектов и определенных действий с его элементами.
Функция высшего порядка может принимать в качестве аргументов другие функции или возвращать функцию в качестве результата. В этом случае функции имеют тот же статус, что и прочие объекты данных. Такой подход позволяет делать сложные программы компактными.

Array.map()

Представим ситуацию, в которой есть несколько записей для студентов с атрибутами: name, ID и marks.

        let studentRecords = [ {name: 'John', id: 123, marks : 98 },
          {name: 'Baba', id: 101, marks : 23 },
          {name: 'yaga', id: 200, marks : 45 },
          {name: 'Wick', id: 115, marks : 75 } ] 
    

Постановка задачи: получить имена студентов и записать их заглавными буквами.

Ожидаемый результат:

        ['JOHN', 'BABA', 'YAGA', 'WICK']
    

Существует несколько способов достижения цели:

        1. for() loop 
let names = [];
for (let index = 0; index < studentRecords.length; index++){
     names.push(studentRecords[index].name.toUpperCase());
}
console.log(names); // logs: [ 'JOHN', 'BABA', 'JOHN', 'WICK' ]

2. for(...of) 
let names = []
for (const student of studentRecords) {
    names.push(student.name.toUpperCase());
}
console.log(names); // logs: [ 'JOHN', 'BABA', 'JOHN', 'WICK' ]

3. forEach()
let names = []
studentRecords.forEach( student => {
     names.push(student.name.toUpperCase());
})
console.log(names); // logs: [ 'JOHN', 'BABA', 'JOHN', 'WICK' ]
    

Во всех вышеупомянутых примерах создается пустой массив для хранения результата. Как в for(...of), так и в for() нужно явно перебирать массивы, что делает код более запутанным.

Решение с помощью map()

        let names = studentRecords.map( stu => stu.name.toUpperCase());
console.log(names); // logs: [ 'JOHN', 'BABA', 'JOHN', 'WICK' ]
    

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

Работа map():

        Array.map( callback, thisValue ) 
    

Метод map() принимает два аргумента: функцию обратного вызова и необязательное значение объекта.

Обратный вызов map() выполняется для каждого элемента исходного массива и возвращает новое значение в результирующий.

Если thisValue не задано, функция обратного вызова будет привязана к объекту, который ее вызвал (это значение зависит от выражения вызывающего объекта). В нашем примере thisValue будет привязываться к studentRecords.

Array.filter()

filter() возвращает только те элементы из массива, которые удовлетворяют заданным критериям.

Постановка задачи: предположим, что у нас есть тот же набор данных, что и выше, но на этот раз мы хотим получить подробную информацию о студентах, набравших более 50 баллов.

Ожидаемый результат:

        [{name: 'John', id: 123, marks : 98 },{name: 'Wick', id: 115, marks : 75 }]
    

Решение: используем filter() для выбора записей, удовлетворяющих заданному условию (т. е. больше 50 баллов).

        let names = studentRecords.filter(stu => stu.marks > 50);
console.log(names);
// logs: [ { name: 'John', id: 123, marks: 98 },{ name: 'Wick', id: 115, marks: 75 }]
    

Постановка задачи: получить информацию о студентах, набравших более 50 баллов и имеющих id больше 120.

        let names = studentRecords.filter(stu => stu.marks > 50 && stu.id > 120)
console.log(names); //logs: [ { name: 'John', id: 123, marks: 98 } ]
    

Решение: как показано в приведенном выше фрагменте кода, используя filter() с несколькими условиями для фильтрации данных, мы можем эффективно справиться с задачей.

Работа filter():

filter() принимает функцию обратного вызова, которая возвращает значение bool. Если она возвращает true, то объект добавляется в результирующий массив. В противном случае объект игнорируется.

Array.reduce()

Точно так же, как map() и filter(), reduce() выполняет обратный вызов для каждого элемента массива. Чтобы лучше понять reduce, сначала нужно рассмотреть два термина: «accumulator» и «reducer».

Accumulator – это значение, которое мы получаем в конечном итоге, а reducer – действие, которое следует выполнить для его получения. Важно помнить, что reducer всегда будет возвращать только одно значение.

Постановка задачи: на этот раз необходимо знать общую сумму баллов студентов.

        let totalMarks = studentRecords.reduce( ((acc,emp) => acc+emp.marks), 0)
console.log(totalMarks);// logs: 241
    

Решение:

        Array.reduce( (accumulator,curr_value) => expression ), intialValue)
    

Работа reduce():

Давайте разберемся в работе reducer шаг за шагом:

  1. reduce() принимает accumulator, текущее значение, выражение и initialValue в качестве параметра, используемого для инициализации accumulator (обратите внимание, что в нашем примере initialValue = 0);
  2. reduce() принимает первый элемент массива в качестве текущего значения и accumulator, а затем выполняет требуемую операцию (как при сложении двух значений в нашем примере) и сохраняет результат в accumulator;
  3. Затем accumulator заменяется ранее рассчитанным accumulator, и текущее значение устанавливается на второй элемент массива. Необходимая операция выполняется и accumulator обновляется с новым результатом.
  4. Шаг 3 выполняется, пока все элементы массива не будут перебраны.
  5. Метод возвращает значение accumulator.

Чтобы сравнить наши знания с фактической работой reduce(), рассмотрим значения «accumulator» и «curr_value» для каждой итерации из приведенного выше примера.

        let totalMarks = 
      studentRecords.reduce( function(accumulator,curr_value){
          console.log(`accumulator: ${accumulator}, curr_value:$                                     curr_value.marks}`);
          return accumulator + curr_value.marks;}, 0)
console.log(totalMarks);
// logs -------
accumulator: 0 , curr_value: 98
accumulator: 98 , curr_value: 23
accumulator: 121 , curr_value: 45
accumulator: 166 , curr_value: 75
241
    

Из журнала видно, что accumulator начинается со значения 0 и на каждом шаге его значение обновляется суммой «previous value» и «curr_value».

Теперь объединим возможности map(), filter() и reduce()

Пример 1. map() и filter()

Постановка задачи: на этот раз мы хотим получить только имена студентов, набравших более 50 баллов из того же набора данных.

        let names = studentRecords.filter(stu => stu.marks > 50).map(stu => stu.name)
console.log(names); // logs: [ 'John', 'Wick' ]
    

Решение: сначала мы отфильтровали данные с помощью filter(), а затем использовали map(), чтобы получить только атрибут name.

Примечание: Вы можете связать map() и filter(), так как они возвращают массивы.

Пример 2. filter() и reduce()

Постановка задачи: напечатаем сумму баллов студентов с id больше 120.

        let totalMarks = studentRecords.filter(stu => stu.id > 120).reduce((acc,curr) => acc + curr.marks ,0)
console.log(totalMarks); // logs: 143
    

Решение: сначала выбрали студентов с идентификатором больше 120 с помощью filter(), а потом передали отфильтрованный массив студентов в reduce(), чтобы получить сумму их баллов.

Примечание: вы можете связать reduce() после map() и filter(), поскольку они возвращают массивы, но не можете связать map() и filter() после reduce(), т. к. он возвращает единственное значение.

Пример 3. map(), filter() и reduce()

Постановка задачи: вывести общее количество студентов с баллами больше 50, полученными после начисления поощрения в 15 баллов.

        let studentRecords = [ {name: 'John', id: 123, marks : 98 },
                  {name: 'Baba', id: 101, marks : 23 },
                  {name: 'John', id: 200, marks : 45 },
                  {name: 'Wick', id: 115, marks : 75 }, ]
let totalMarks = studentRecords.map(function(stu){ 
            if(stu.marks < 50){
                stu.marks += 15;}
            return stu;
        }).filter(stu => stu.marks > 50).reduce((acc,curr) => acc+curr.marks, 0);
console.log(totalMarks); //logs : 233
    

Решение:

  1. Используя map(), добавляется поощрение в 15 баллов студентам, набравшим менее 50 баллов.
  2. Затем, используем filter() для массива студентов, возвращаемого функцией map(). Так мы найдем всех студентов с отметками больше 50.
  3. Наконец, мы использовали функцию reduce() для возвращаемого функцией filter() массива студентов, чтобы вернуть сумму оценок.

Заключение

В этом гайде мы разобрались, как map(), filter() и reduce() могут облегчить жизнь разработчику, сократив количество ненужных явных циклов и объявлений пустых массивов. Это сделает код более функциональным и легким для понимания. Попробуйте заменять циклы этими методами всякий раз, когда у вас появится такая возможность. Удачи!

Дополнительные материалы:

Источники

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG