company_banner

Начало работы с методами JavaScript-массивов .map(), .filter() и .reduce()

Автор оригинала: Jonathan Hsu
  • Перевод
Когда я разбирался в том, как пользоваться методами JS-массивов .map(), .filter() и .reduce(), всё, что я читал, смотрел и слушал, казалось мне очень сложным. Эти концепции рассматривались как некие самостоятельные механизмы, ни к чему другому отношения не имеющие. Мне тяжело было ухватить их суть и их понять.



Я слышал, что это — базовые вещи, понимание которых является чем-то вроде границы между «посвящёнными» и «непосвящёнными». Хотелось бы мне тогда, чтобы мне сказали о них правду. Она заключается в том, что эти три метода символизируют то, что причины, по которым перебирают некие итерируемые объекты, часто вписываются в одну из трёх функциональных категорий.

Просматривая код, который я писал раньше, я понял, что в 95% случаев, когда я перебирал элементы строк или массивов, я выполнял одно из следующих действий:

  • Применение к каждому значению некоей последовательности инструкций (аналог метода .map()).
  • Фильтрация значений, соответствующих заданному критерию (то же, что делает .filter()).
  • Сведение набора данных к единственному агрегированному значению (аналог .reduce()).

Это был момент истины. Именно тогда я понял суть этих методов и увидел их связь с тем, что мне уже давно известно.

Для того чтобы попрактиковаться, я взял свой старый код и отрефакторил его с использованием этих методов. Это оказалось весьма полезным занятием.

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

Метод .map()


Метод .map() используется в том случае, если нужно сделать следующее:

  1. Надо выполнить над каждым элементом итерируемого объекта некую последовательность действий.
  2. Надо вернуть значение, которое, предположительно, было изменено.

Рассмотрим простой пример, в котором для каждого элемента массива, содержащего цены, нужно найти новые суммы, включающие в себя изначальные цены и налог с продаж:

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()?


RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Похожие публикации

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

    +2
    Проблема в восприятии новых функций/конструкций кроется в отсутствии ассоциаций с чем-либо уже понятым. Расписали отлично, спасибо!
      –7
      Не пользуюсь. Специфика моего JS в том, что он запускается в IE разных версий и в некоторых версиях ещё не поддерживается…
      Когда столкнулся со стрелочной функцией в чужом коде, довольно долго втыкал, что это такое… Хоть концепция лямбд мне и была знакома, но в JS она как — то слишком лаконична мне показалась…
        +1
        IE разных версий и в некоторых версиях ещё не поддерживается…

        Используйте babel + browserify, и ваш код будет чистым, современным, поддерживаемым. А если вдруг в вашей компании случится автобусный инцидент, вы будете востребованы на рынке.

          +3
          Специфика моего JS в том, что он запускается в IE разных версий и в некоторых версиях ещё не поддерживается…
          С прискорбием спешу сообщить, что уже и не будет.
            0
            Ну это то понятно, что не будет…
            IE встраивается в конкретное приложение и на форме приложения выводит интерфейс.
            Приложение писано не нами и нет внятных перспектив, что IE поменяют на что то ещё…
              +1
              Ну так полифилы есть.
              Хозяин-барин, конечно, но в сегодняшних реалиях писать код с оглядкой на браузер это либо совсем трудные случаи — canvas vs svg vs vml там, либо особое удовлетворение с этого иметь.
            0
            Лично я не пользуюсь, потому что в процессе разработки приходится то добавлять, то убирать команды внутри callback-функции. И индекс иногда то нужен, то нет.
            Постоянно переписывать for на map и обратно — задалбывает.
            А такие простые случаи, что указаны в посте, редко встречаются.
            PS: к нижнему комментарию: если всё изначально указывать, то размер кода не сильно меньше, чем у for. Зато ещё эта скобка после }…
            PPS: и постоянно чередовать стиль то for, то map — тоже как-то не очень.
              +2
              И индекс иногда то нужен, то нет.

              В callbackFn мапа вообще-то есть индекс.
                +1
                то убирать команды внутри callback-функции

                В чём проблема? Если неудобно со стрелочной функцией — передайте туда обычную.
                  0

                  Вы можете передать индекс в колбэк как второй параметр. В случае reduce — как третий.

                    0

                    Чтоб два раза не вставать: ещё в колбэк можно передать исходный массив.

                    +2

                    Алло, указанные в статье функции поддерживаются начиная с IE9. О какой поддержке вообще идет речь?

                    –4
                    Есть особенность map (и foreach), которую я не знал и столкнулся с проблемами — нет гарантии, что элементы будут обработаны последовательно. В моем случае просто переделал на стандартный цикл For и все встало на свои места
                      +2
                      Да что вы говорите.

                      А вот стандарт говорит обратное.
                        +1
                        Хмм, тогда приношу свои извинения, видимо проблема была в асинхронности
                      0
                      Забыли добавить один нюанс. Если в методе .reduce() явно не задать значение в аккумулятор, то для аккумулятора будет использовано, первое значение переданного массива.
                        –1
                        А у меня обычно в коллбеке ошибка возникала, если я забывал аккумулятор указать.
                          0
                          Возможно имелось в виду, не второй атрибут колбека — он как раз обязателен, а второй опциональный параметр, после колбека, который содержит начальное значение аккумулятора.
                          В примере с donations как раз такой случай и показан.
                            +1
                            он как раз обязателен

                            Да сфигали? Ничего там не обязательно вообще.
                            [1,2,3].reduce(() => { console.log(`woo!`); })
                              0

                              Ну с точки зрения языка — нет.
                              А с целью суммирования элементов массива — можно пример без него?

                                0
                                Ну разумеется можно, потому что там еще два параметра есть:
                                [1,2,3,4].reduce((acc, _, index, src) => acc + src[index]);

                                но конечно не рекомендую так где-бы то ни было извращаться ради суммирования.
                              0
                              Я понял о чём речь, просто по воле случая, все мои редьюсеры критично зависели от валидной инициализации аккумулятора. Я сейчас напряжённо думаю, как так вышло…
                          0

                          Если привыкнуть, то выглядит очень удобно. Коротко и элегантно, но для меня все эти JS фишки никогда не будут удобочитаемыми.
                          Ключевое for сразу подразумевает о перечислении, независимо от языка и примерный смысл можно понять даже если с языком не работал, а в специфические сокращения у JS иногда приходится долго втыкать, если до этого плотно сидел на каком-нибудь python

                            +1
                            в специфические сокращения у JS иногда приходится долго втыкать, если до этого плотно сидел на каком-нибудь python

                            Наоборот — тоже.
                              +1
                              map, filter и reduce — базовые функции, которые есть в любом высокоуровневом языке, предоставляющем возможности функционального программирования. и python — не исключение. не понимаю, о каких «специфических сокращениях» идет речь…
                              подобное можно сказать только о яваскриптовом [].forEach(). тут соглашусь — действительно выглядит как костыль
                              +1
                              const donations = [5, 20, 100, 80, 75];
                              let total = donations.reduce((total,donation) => {
                                 return total + donation;
                              });
                              

                              const donations = [5, 20, 100, 80, 75];
                              let total = donations.reduce((total,donation) => {
                                 return total + donation;
                              }, 450);
                              


                              Я бы вкинул вам свои 5 копеек :-), функция reduce в вашем случае дважды делает операцию сложения можно было на втором примере сказать что функция сложения может быть сохранена в переменную с идентификатором add и потом многократно использована в разных инструкциях где нужно сложение.

                              const donations = [5, 20, 100, 80, 75];
                              const add = (a,b) => a+ b;
                              let total = donations.reduce(add, 450);
                              


                              Не увидел и еще одного из важнейших профитов от map, reduce, filter, sort — chain

                              // массив возрастов
                              // цель взять совершеннолетних и получить общий возраст :-)
                              const isAdult = age => age > 18;
                              const add = (a, b) => a + b;
                               [15, 20, 60, 80, 95, 50, 45, 35].filter(isAdult).reduce(add, 0); // => 385
                              


                              Та же я не услышал что все эти функции никак не изменяют изначальный массив и возвращают новый экземпляр массива.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое