Когда я разбирался в том, как пользоваться методами 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()?


Комментарии (25)


  1. bp16
    20.12.2019 12:55
    +2

    Проблема в восприятии новых функций/конструкций кроется в отсутствии ассоциаций с чем-либо уже понятым. Расписали отлично, спасибо!


  1. RUSich101
    20.12.2019 13:06
    -2

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


    1. Carduelis
      20.12.2019 13:14

      IE разных версий и в некоторых версиях ещё не поддерживается…

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


    1. vvadzim
      20.12.2019 13:16
      +1

      Специфика моего JS в том, что он запускается в IE разных версий и в некоторых версиях ещё не поддерживается…
      С прискорбием спешу сообщить, что уже и не будет.


      1. RUSich101
        20.12.2019 13:52

        Ну это то понятно, что не будет…
        IE встраивается в конкретное приложение и на форме приложения выводит интерфейс.
        Приложение писано не нами и нет внятных перспектив, что IE поменяют на что то ещё…


        1. vvadzim
          20.12.2019 14:22

          Ну так полифилы есть.
          Хозяин-барин, конечно, но в сегодняшних реалиях писать код с оглядкой на браузер это либо совсем трудные случаи — canvas vs svg vs vml там, либо особое удовлетворение с этого иметь.


    1. YuriM1983
      20.12.2019 14:24

      Лично я не пользуюсь, потому что в процессе разработки приходится то добавлять, то убирать команды внутри callback-функции. И индекс иногда то нужен, то нет.
      Постоянно переписывать for на map и обратно — задалбывает.
      А такие простые случаи, что указаны в посте, редко встречаются.


      1. JustDont
        20.12.2019 14:28
        +1

        И индекс иногда то нужен, то нет.

        В callbackFn мапа вообще-то есть индекс.


      1. Zenitchik
        20.12.2019 15:24

        то убирать команды внутри callback-функции

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


      1. maggg
        20.12.2019 20:19

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


      1. maggg
        20.12.2019 20:20

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


    1. justboris
      20.12.2019 20:18
      +1

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


  1. latonita
    20.12.2019 14:06

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


    1. JustDont
      20.12.2019 14:22

      Да что вы говорите.

      А вот стандарт говорит обратное.


      1. latonita
        20.12.2019 16:05

        Хмм, тогда приношу свои извинения, видимо проблема была в асинхронности


  1. rampels
    20.12.2019 15:15

    Забыли добавить один нюанс. Если в методе .reduce() явно не задать значение в аккумулятор, то для аккумулятора будет использовано, первое значение переданного массива.


    1. Zenitchik
      20.12.2019 15:26
      -1

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


      1. Valery4
        20.12.2019 18:46

        Возможно имелось в виду, не второй атрибут колбека — он как раз обязателен, а второй опциональный параметр, после колбека, который содержит начальное значение аккумулятора.
        В примере с donations как раз такой случай и показан.


        1. JustDont
          20.12.2019 19:17

          он как раз обязателен

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


          1. Valery4
            20.12.2019 19:22

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


            1. JustDont
              20.12.2019 19:36

              Ну разумеется можно, потому что там еще два параметра есть:

              [1,2,3,4].reduce((acc, _, index, src) => acc + src[index]);

              но конечно не рекомендую так где-бы то ни было извращаться ради суммирования.


        1. Zenitchik
          20.12.2019 22:33

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


  1. razielvamp
    20.12.2019 16:47

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


    1. Zenitchik
      20.12.2019 17:02

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

      Наоборот — тоже.


    1. zoryamba
      22.12.2019 13:58

      map, filter и reduce — базовые функции, которые есть в любом высокоуровневом языке, предоставляющем возможности функционального программирования. и python — не исключение. не понимаю, о каких «специфических сокращениях» идет речь…
      подобное можно сказать только о яваскриптовом [].forEach(). тут соглашусь — действительно выглядит как костыль