Автор материала, перевод которого мы публикуем сегодня, говорит, что в последние несколько месяцев ему, при проверке пулл-реквестов, постоянно попадались одни и те же четыре недочёта, связанных с нерациональным использованием методов массивов в 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?
Автор: ru_vds