Эффективное использование методов массивов в JavaScript

https://medium.freecodecamp.org/heres-how-you-can-make-better-use-of-javascript-arrays-3efd6395af3c
  • Перевод
Автор материала, перевод которого мы публикуем сегодня, говорит, что в последние несколько месяцев ему, при проверке пулл-реквестов, постоянно попадались одни и те же четыре недочёта, связанных с нерациональным использованием методов массивов в JavaScript. Для того чтобы таких недостатков кода, которые раньше появлялись и в его программах, стало меньше, он и написал эту статью.



Замена indexOf() на includes()


«Если вы ищите что-то в массиве, используйте метод indexOf()». Примерно такая рекомендация мне встретилась на одном из курсов, когда я изучал JavaScript. Рекомендация это вполне нормальная, ничего плохого о ней сказать нельзя.

На MDN можно узнать, что метод indexOf() возвращает первый индекс, по которому некий элемент может быть найден в массиве. Это значит, что если мы планируем использовать в программе этот индекс, метод indexof() отлично подходит для поиска элементов в массивах.

А что если нам всего лишь нужно узнать, есть ли некий элемент в массиве или нет? То есть, нас интересует не индекс этого элемента, если он есть в массиве, а сам факт его наличия или отсутствия. При таком подходе нас вполне устроит команда, которая возвращает true или false. В подобных случаях я рекомендую пользоваться не методом indexOf(), а методом includes(), который возвращает логическое значение. Рассмотрим пример:

'use strict';

const characters = [
  'ironman',
  'black_widow',
  'hulk',
  'captain_america',
  'hulk',
  'thor',
];

console.log(characters.indexOf('hulk'));
// 2
console.log(characters.indexOf('batman'));
// -1

console.log(characters.includes('hulk'));
// true
console.log(characters.includes('batman'));
// false

Использование метода find() вместо метода filter()


Метод filter() — весьма полезный инструмент. Он, на основе одного массива, создаёт другой массив, содержащий элементы исходного массива, соответствующие заданному условию. Как можно понять из имени этого метода, он предназначен для фильтрации массивов, в ходе которой обычно получаются массивы, имеющие меньшую длину, чем исходные.

Как быть, если мы знаем, что после фильтрации массива останется всего один элемент? Например, такое может произойти при попытке отфильтровать элементы массива на основе некоего уникального идентификатора. В такой ситуации я не советовал бы пользоваться методом filter(), так как тот массив, который он сформирует, будет содержать всего один элемент. Если нас интересует элемент массива с уникальным значением, то работать мы собираемся с единственным значением, а для представления такого значения массив не нужен.

Если говорить о производительности метода filter(), то окажется, что ему, для формирования списка из элементов, соответствующих заданному при его вызове условию, придётся просмотреть весь массив. Более того, представим, что в массиве имеются сотни элементов, удовлетворяющих заданному условию. Это приведёт к тому, что результирующий массив окажется довольно большим.

Для того чтобы избежать попадания в подобные ситуации, я посоветовал бы использовать метод find(). При вызове ему передаётся коллбэк, описывающий условие, очень похожий на тот, который используется с методом filter(), но метод find() возвращает лишь первый элемент, соответствующий условию. При этом данный метод останавливает работу сразу после того, как найдёт такой элемент. Весь массив ему, в итоге, просматривать не приходится.

'use strict';

const characters = [
  { id: 1, name: 'ironman' },
  { id: 2, name: 'black_widow' },
  { id: 3, name: 'captain_america' },
  { id: 4, name: 'captain_america' },
];

function getCharacter(name) {
  return character => character.name === name;
}

console.log(characters.filter(getCharacter('captain_america')));
// [
//   { id: 3, name: 'captain_america' },
//   { id: 4, name: 'captain_america' },
// ]

console.log(characters.find(getCharacter('captain_america')));
// { id: 3, name: 'captain_america' }

Замена метода find() на метод some()


Должен признать, ту оплошность, которую мы сейчас обсудим, я совершал много раз. Потом мне посоветовали заглянуть на MDN и посмотреть, как улучшить то, что я делал нерационально. Если в двух словах, то это очень похоже на то, что мы только что рассматривали, говоря о методах indexOf() и includes().

В вышеописанном случае мы видели, что метод find(), в качестве аргумента, принимает коллбэк и возвращает элемент массива. Можно ли назвать метод find() наиболее удачным решением в том случае, если нам надо узнать, содержит ли массив некое значение или нет? Возможно — нет, так как этот метод возвращает значение элемента массива, а не логическое значение.

В подобной ситуации я порекомендовал бы воспользоваться методом some(), который возвращает логическое значение.

'use strict';

const characters = [
  { id: 1, name: 'ironman', env: 'marvel' },
  { id: 2, name: 'black_widow', env: 'marvel' },
  { id: 3, name: 'wonder_woman', env: 'dc_comics' },
];

function hasCharacterFrom(env) {
  return character => character.env === env;
}

console.log(characters.find(hasCharacterFrom('marvel')));
// { id: 1, name: 'ironman', env: 'marvel' }

console.log(characters.some(hasCharacterFrom('marvel')));
// true

Использование метода reduce() вместо комбинации методов filter() и map()


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

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

Для того, чтобы снизить нагрузку на систему, создаваемую использованием двух методов и повысить производительность программ, в подобных случаях я посоветовал бы использовать метод reduce(). Результат будет таким же самым, а код получится лучше. Этот метод позволяет фильтровать интересующие нас элементы и добавлять их в аккумулятор. Аккумулятором может быть числовая переменная, хранящая, скажем, сумму элементов массива, это может быть объект, строка или массив, в которых можно накапливать нужные нам элементы.

В нашем случае, так как речь идёт об использовании метода map(), я посоветовал бы использовать метод reduce() с массивом в качестве аккумулятора. В следующем примере мы фильтруем элементы массива, являющиеся объектами, по значению поля env, и выполняем их преобразование.

'use strict';

const characters = [
  { name: 'ironman', env: 'marvel' },
  { name: 'black_widow', env: 'marvel' },
  { name: 'wonder_woman', env: 'dc_comics' },
];

console.log(
  characters
    .filter(character => character.env === 'marvel')
    .map(character => Object.assign({}, character, { alsoSeenIn: ['Avengers'] }))
);
// [
//   { name: 'ironman', env: 'marvel', alsoSeenIn: ['Avengers'] },
//   { name: 'black_widow', env: 'marvel', alsoSeenIn: ['Avengers'] }
// ]

console.log(
  characters
    .reduce((acc, character) => {
      return character.env === 'marvel'
        ? acc.concat(Object.assign({}, character, { alsoSeenIn: ['Avengers'] }))
        : acc;
    }, [])
)
// [
//   { name: 'ironman', env: 'marvel', alsoSeenIn: ['Avengers'] },
//   { name: 'black_widow', env: 'marvel', alsoSeenIn: ['Avengers'] }
// ]

Итоги


В этом материале мы рассмотрели некоторые подходы к эффективному использованию методов массивов при решении различных задач. Полагаем, идеи, на которых основаны рекомендации, данные автором этой статьи, могут помочь в улучшении JS-кода и в других ситуациях.

Уважаемые читатели! Доводилось ли вам встречаться с примерами нерационального использования механизмов JavaScript?

RUVDS.com

708,39

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

Поделиться публикацией
Комментарии 36
    +4
    Решил проверить последний пункт, чтобы выяснить, стоит ли игра свеч — код с filter+map мне визуально нравится намного больше, чем с reduce. Тестовый кейс, результаты в хроме на маке — reduce в 4 раза медленнее.

    /здесь недовольное бормотание про преждевременную оптимизацию и вредные советы/
      0

      а с for...of?

        +1
        Нахлобучил туда же еще for...of и обычный цикл — на моей машине оба варианта быстрее, чем filter+map, но не намного. Обычный цикл предсказуемо впереди всех.

        Подозреваю, что filter+map не сильно отстает просто потому, что конструкция очень часто встречается и, видимо, очень сильно оптимизирована в V8.
          0
          в лисе все варианты примерно одинаковы, кроме тормозного concat().

          в хромом reduce обогнал даже цикл.

          как всегда отличился edge. в нем for...of на 65% тормознее обычного for. *рукалицо*

          возможно, современные движки преобразуют подобные простые примеры в обычный цикл. или новые массивы так быстро создаются? правда, не знаю, насколько актуален массив из 3-х элементов.
        0
        Можете добавить такой вариант для сравнения?
        const processed = characters
        .reduce((acc, character) => {
        return character.env === 'marvel'
        ? (acc.push(Object.assign({}, character, { alsoSeenIn: ['Avengers'] })), acc)
        : acc;
        }, [])
          0
          Добавил. Результаты уравнялись :)
          Все равно выходит, что на массивах подобной длины смысла использовать reduce особо нет.
            0

            Я не могу почему-то добавить, но мне кажется, такой вариант будет не медленнее, а может даже быстрее.


            const processed = [];
            
            characters.forEach((el) => {
               if(el.env === 'marvel') {
                  processed.push(Object.assign({}, characters[i], { alsoSeenIn: ['Avengers'] }));
               }
            });
          0
          Не только же хромом единым. В Edge reduce не самый медленный, а вот for..of на удивление у меня тормозит знатно. В FF последнем разрыв уже не такой большой.
            +4

            Если писать reduce так как автор, то ни о какой производительности и речи не может быть. Из статьи в статью одна и та же ошибка. Вот такое решение:


            reduce((arr, el) => arr.concat(...), [])

            в каждой итерации reduce arr.concat создаёт НОВЫЙ МАССИВ! Это бессмысленное перекладывание байтов! Big O такого решения чудовищно! Чем больше наш arr.length, тем чудовищнее будет результат.

            +1

            Шипилева на вас нет!


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


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


            P.S. Ещё раз повторюсь, думаю, что на фронте не должно возникать ситуаций, когда это реально критично. Может какая-то виртуальная реальность или игры, но тогда императивный стиль в помощь.

            +1
            indexOf vs includes
            А чего так завязывать на возврат логическое ответа?!
            И если так уж надо логический ответ:
            console.log(!!~characters.indexOf('hulk'));
            // true
            console.log(!!~characters.indexOf('batman'));
            // false


            filter vs find
            В варианте с find потерян один вариант в выборке, значит результат работы кода эквивалентен. А значит подобный пример и замена вообще неуместны.

            ИМХО поиск и решение проблем там где их нет. Цель подобных примеров: смотрите у нас появились новые методы, давайте их использовать везде. Может надо просто быть объективней.

            Статья ради статьи…
              +3
              Цель подобных примеров: смотрите у нас появились новые методы, давайте их использовать везде

              Мне показалось наоборот.
              : Смотрите, у нас появился метод, который проверяет есть ли элемент в массиве! Так давайте indexOf использовать по своему назначению (для получения индекса), а includes по своему (для определения, существует ли такой элемент).


              А за вот такое неявное добро
              !!~
              надо наказывать.


              Может надо просто быть объективней.

              Как раз и получается, что нужно быть объективнее, и использовать методы по их назначению, а не получать массив элементов через filter только для того, чтобы получить один элемент.
              Пример с reduce и map/filter плохой.

                +1

                Скажите, а вы на полном серьёзе считаете что !!~indexOf и includes равно-уместны в реальном коде?

                  –1
                  Нет конечно, я лишь обратил внимание что замена ради получения логического выражения это абсурд.
                    0

                    Обычный рефакторинг\"причёсывание кода", почему абсурд?

                  0
                  Статья ради статьи…
                  Ну вообще-то ради рекламы)
                  +5
                  Какой-то неудачный пример reduce. reduce — это же не про filter+map в одном, это же про агрегацию — на входе итератор, на выходе одно значение:
                  let values = [3,2,60,4];
                  let sum = values.reduce((s,v)=>s+v);
                  //sum = 69
                  

                  Метод универсальный, но использование его вместо filter/map/some/find — это всё равно, что использовать indexOf вместо includes, против чего выступают в начале статьи.
                    0
                    использование reduce как замена filter + map сильно (в 2 раза) помогает сократить количество итераций
                      0

                      forEach еще сильнее.

                        0
                        можно пример?
                          0

                          Я немного ошибся с формулировкой, имею ввиду правильнее. Итераций будет столько же.


                          const processed = [];
                          
                          characters.forEach((el) => {
                             if(el.env === 'marvel') {
                                processed.push(Object.assign({}, characters[i], { alsoSeenIn: ['Avengers'] }));
                             }
                          });

                          но использование reduce через concat или https://habr.com/company/ruvds/blog/422091/#comment_19064761 мне кажется, как минимум не логично, т.к. код с forEach получается точно такой же, а в примере reduce используется тупо для интегрирования, с этой задачей справляется и forEach.

                            0
                            в данном контексте для меня reduce выглядит гораздо чище, хотя это вопрос вкуса :-)
                        0
                        Даже если допустить, что reduce работает в два раза быстрее, чем filter + map, то для большинства проектов — это все равно не нужная микро-оптимизация, которая вообще не будет заметна. А читаемость кода страдает.
                          0
                          возможно…
                      0
                      Всегда нужно смотреть, какая получается выгода. Если элемент ищется один раз даже в очень большом массиве после клика пользователя, то пользователю абсолютно все равно, отработает код за 1 миллисекунду или за 100 миллисекунд, и в этих случаях лучше писать как удобно.

                      Когда же скорость по настоящему критична, например когда поиск происходит часто и много раз, то имеет смысл просто сразу индексировать массив и находить значения моментально. В случае когда необходимо булево значение вообще использовать Object или Set.

                      Случаев, когда замена indexOf на includes или filter + map на reduce принесет ощутимую пользовательскому глазу выгоду не так много, к тому же методы разработчиками браузеров постоянно допиливаются и показатели меняются. Можно вдруг обнаружить, что filter + map в ряде случаев быстрее, чем reduce, а простой перебор цикла еще быстрее. Посему для кода использовать что более удобно — например includes использовать просто логичней и красивей, когда нужно булево значение, чем indexOf !== false, а для быстрого поиска использовать индексацию.
                        0
                        Ну, если говорить о скорости — while/for+break будет в любом случае быстрее чем любой функциональный вариант (filter/some e.t.c.). Но это императивный код.

                        С includes все еще есть проблема, что babel не транспайлит его во что-то более менее адекватное.
                        image
                        А это EcmaScript 2016. Поэтому нужно быть очень аккуратным и использовать его только там где вы уверены будет поддержка ES7 (сервер/приложение для вечнозеленых браузеров).
                          +1

                          Дык Babel и не должен транспайлить includes. Это задача полифилов. Собственно использование одного только транспайлера, без связки полифилов, это как-минимум очень странно :)

                        0

                        Если можете поясните новичку почему так acc.push(Object.assign({}, character, { alsoSeenIn: ['Avengers'] }) не работае, а вот так (acc.push(Object.assign({}, character, { alsoSeenIn: ['Avengers'] })), acc) работает? Ведь acc это массив, но в первом варианте выдает ошибку что push is not a function. Заранее спасибо)

                          +1

                          Всё просто:


                          // variant 1
                          return do1(), do2(), do3(), do4(), 5;
                          
                          // variant 2
                          do1();
                          do2();
                          do3();
                          do4();
                          return 5;

                          Оба участка кода эквивалентны. Автор приведённой вами строки сэкономил символы используя "оператор запятую". Но имейте ввиду, что обычно за такой код "бьют", и никаких лишних мутных запятых в коде быть не должно.

                          +1

                          Очередная плохая статья от RUVDS, наверное, они решили что таким способом круто продвигаться. Может у них результат и хороший от этого, но понимаю, почему не переводить интересные статьи.


                          По сути.


                          1. "Замена indexOf() на includes()"
                            Конечно, якобы второй метод лучше говорит сам за себя, но первый является общепринятым, отсюда, также понятным.


                          2. "Использование метода find() вместо метода filter()" и "Замена метода find() на метод some()"
                            Тут понятно, все зависит от того, что нужно сделать. Странное замечание в общем.


                          3. "Использование метода reduce() вместо комбинации методов filter() и map()"
                            Ну это вообще тупость, чувак явно дома забивает гвозди шуруповёртом (хотя может и шурупы им забивает).


                            0
                            Если вы знаете реально крутые посты, качественные переводы готовы неистово плюсовать — присылайте в личку, посмотрим.
                              0

                              А какая тематика интересна? И какой формат?

                                0
                                Интересны обучающие материалы по работе с языками программирования типа JS, по веб разработке, по администрированию и настройке виртуальных серверов, по установке и работе с различными программами на виртуальных серверах на ОС Windows Server или Linux
                                В общем присылайте в личку все, что вы считаете интересным для перевода. Достаточно просто ссылки на материал.

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

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