Предлагаем вашему вниманию переводной материал об использовании map и reduce в функциональном JavaScript. Эта статья будет интересна в первую очередь начинающим разработчикам.
За всеми этими разговорами о новых стандартах легко забыть о том, что именно ECMAScript 5 подарил нам ряд инструментов, благодаря которым мы сегодня можем использовать функциональное программирование в JavaScript. Например, нативные методы map() и reduce() на базе JS-объекта Array
. Если вы до сих пор не пользуетесь map()
и reduce()
, то сейчас самое время начать. Наиболее современные JS-платформы нативно поддерживают ECMAScript 5. Использование этих методов позволит сделать ваш код гораздо чище, читабельнее и удобнее в обслуживании. map()
и reduce()
помогут вам встать на путь более элегантной функциональной разработки.
Замечание по производительности
Безусловно, читабельность и обслуживаемость кода не должны снижать производительность, если этого требует ситуация. Современные браузеры эффективнее выполняют более громоздкие традиционные конструкции, например, циклы.
Попробуйте следующую методику: сначала напишите код, исходя из критериев читабельности и обслуживаемости, а затем оптимизируйте его производительность, если в этом есть реальная потребность. Избегайте преждевременной оптимизации.
Также стоит отметить, что применение методов наподобие map()
и reduce()
позволит извлечь больше преимуществ из улучшений JS-движка, по мере того, как браузеры будут оптимизироваться для их использования. Если у вас нет проблем с производительностью, то лучше писать код с оптимистическим расчётом на будущее. А приёмы повышения производительности, делающие код менее опрятным, оставьте на потом, когда в них возникнет потребность.
Использование map
Маппинг — фундаментальная методика в функциональном программировании. Она применяется для оперирования всеми элементами массива с целью создания другого массива той же длины, но с преобразованным содержимым.
Чтобы было понятнее, давайте рассмотрим простой пример. Допустим, у вас есть массив слов, и вам нужно преобразовать его в массив, содержащий длины всех слов исходного массива. Да, это не самый актуальный случай, но понимание того, как работает этот инструмент, поможет вам применять его в дальнейшем для улучшения своего кода.
Вероятно, вы уже знаете, как выполнить описанную задачу с помощью цикла for
. Например, так:
var animals = ["cat","dog","fish"];
var lengths = [];
var item;
var count;
var loops = animals.length;
for (count = 0; count < loops; count++){
item = animals[count];
lengths.push(item.length);
}
console.log(lengths); //[3, 3, 4]
Здесь определено несколько переменных:
- массив
animals
содержит исходные слова, - пустой массив
lengths
будет содержать результаты выполнения операции, - переменная
item
используется для временного хранения каждого из элементов массива, которым мы манипулируем во время выполнения каждого цикла, - массив
for
содержит внутреннюю переменнуюcount
и оптимизирован с помощью переменной loops.
Далее мы итерируем каждый элемент массива animals
: вычисляем длину и помещаем в массив lengths
.
Примечание: эту задачу можно было бы решить лаконичнее, без переменной элемента и промежуточного присваивания, передавая длину animals[count]
напрямую в массив lengths
. Код стал бы немного короче, но и менее читабельным даже в этом простом примере. Аналогично, чтобы слегка повысить производительность, можно было бы использовать известную длину массива animals
для инициализации массива lengths
как new Array(animals.length)
, а затем вместо применения push
внести элементы по индексу. Но это тоже сделало бы код немного менее понятным. В общем, всё зависит от того, как вы будете использовать свой код в реальных проектах.
Технически это правильный подход. Он будет работать на любом стандартном JS-движке. Но когда вы узнаете про map()
, то классический способ сразу покажется слишком громоздким.
Вот как можно решить нашу задачу с помощью map()
:
var animals = ["cat","dog","fish"];
var lengths = animals.map(function(animal) {
return animal.length;
});
console.log(lengths); //[3, 3, 4]
Здесь мы опять начинаем с переменной для массива animals
. Но кроме неё мы объявляем только lengths
, и напрямую присваиваем ей результат, полученный при маппинге анонимной встраиваемой функции в каждый элемент массива animals
. Анонимная функция выполняет операцию по каждому животному и возвращает длину. В конце концов массив lengths
, содержащий длины каждого слова, становится такой же длины, как и исходный animals
.
Обратите внимание, что при таком подходе:
- Код получается гораздо короче.
- Нужно объявлять гораздо меньше переменных. Следовательно, мы создаём меньше шума в глобальном пространстве имён, снижая вероятность возникновения коллизий, если другая часть того же кода использует переменные с теми же именами.
- Ни одной переменной не нужно менять своё значение от начала и до конца цикла. По мере изучения функционального программирования вы будете всё больше ценить изящную силу использования констант и неизменяемых переменных. И начинать никогда не поздно.
Ещё одно преимущество этого подхода заключается в том, что мы можем сделать его гибче, разделив именованную функцию. При этом код станет чище. Анонимные встраиваемые функции затрудняют повторное использование кода и могут выглядеть неопрятно. Можно было бы определить именованную функцию getLength()
и использовать её следующим образом:
var animals = ["cat","dog","fish"];
function getLength(word) {
return word.length;
}
console.log(animals.map(getLength)); //[3, 3, 4]
Посмотрите, насколько чище стал выглядеть код. Просто начните применять маппинг, и сразу выйдете на новый уровень функционального программирования.
Что такое функтор?
Любопытно, что при добавлении маппинга в объект массива, ECMAScript 5 превращает основной тип массива в полный функтор. Это делает функциональное программирование ещё более доступным.
Согласно классическим определениям функционального программирования, функтор удовлетворяет трём критериям:
- Содержит набор значений.
- Реализует функцию
map
для оперирования каждым элементом. - Функция
map
возвращает функтор того же размера.
Если хотите больше узнать о функторах, можете посмотреть видео Маттиаса Питера Йоханссона.
Использование reduce
Метод reduce() впервые появился в ECMAScript 5. Он аналогичен map()
, за исключением того, что вместо создания другого функтора reduce()
производит единичный результат, который может быть любого типа. Например, вам нужно получить в виде числа сумму длин всех слов в массиве animals
. Вероятно, вы сразу напишете примерно так:
var animals = ["cat","dog","fish"];
var total = 0;
var item;
for (var count = 0, loops = animals.length; count < loops; count++){
item = animals[count];
total += item.length;
}
console.log(total); //10
После описания начального массива, мы создаём переменную total
для подсчёта суммы, и присваиваем ей ноль. Также создаём переменную item
, в которой, по мере выполнения цикла for
, сохраняется результат каждой итерации над массивом animals
. В качестве счётчика циклов используется переменная count
, а loops
применяется для оптимизации итераций. Запускаем цикл for
, итерируем все слова в массиве animals
, присваивая значение каждого из них переменной item
, и прибавляем длины слов к нарастающему итогу.
Опять же, чисто технически здесь всё в порядке. Обработали массив, получили результат. Но с помощью метода reduce()
можно это сделать гораздо проще:
var animals = ["cat","dog","fish"];
var total = animals.reduce(function(sum, word) {
return sum + word.length;
}, 0);
console.log(total);
Здесь мы определяем новую переменную total
и присваиваем ей значение результата, полученного после применения reduce
к массиву animals
с использованием двух параметров: анонимной встраиваемой функции и нарастающего итога. reduce
берёт каждый элемент массива, применяет к нему функцию и добавляет получаемый результат к нарастающему итогу, которая затем передаётся в следующую итерацию. Подставляемая функция получает два параметра: нарастающий итог и текущее обрабатываемое слово из массива. К длине этого слова функция добавляет текущее значение total
.
Обратите внимание, что мы обнуляем второй аргумент reduce()
, значит total
является числом. Метод reduce()
будет работать и без второго аргумента, но результат может отличаться от ожидаемого. Попробуйте сами определить, какую логику использует JavaScript при исключении total
.
Возможно, описанный подход выглядит слишком сложным. Это следствие интегрированного определения встраиваемой функции в вызываемом методе reduce()
. Давайте зададим именованную функцию вместо анонимной встраиваемой:
var animals = ["cat","dog","fish"];
var addLength = function(sum, word) {
return sum + word.length;
};
var total = animals.reduce(addLength, 0);
console.log(total);
Получается немного длиннее, но это не всегда недостаток. В данном случае становится понятнее, что происходит с методом reduce()
. Он получает два параметра: функцию, которая применяется к каждому элементу массива, и начальное значение нарастающего итога. В данном случае мы передаём имя новой функции addLength
начальное значение (нулевое) нарастающего итога. Функция addLength()
также получает два параметра: нарастающий итог и строковое значение.
Заключение
Привыкнув регулярно использовать map()
и reduce()
, вы сможете сделать свой код чище, гибче и легче в обслуживании. Это откроет вам дорогу к использованию других функциональных подходов в JavaScript.
Помимо map()
и reduce()
в ECMAScript 5 появились и другие новые методы. Вероятно, улучшение качества кода и удовольствие от разработки, которое вы прочувствуете, намного перевесят временное ухудшение производительности. Используйте функциональные подходы и измеряйте влияние на производительность в реальных проектах, вместо того, чтобы думать, а нужны ли map()
и reduce()
в вашем приложении.
Автор: NIX Solutions