javascript

Использование map и reduce в функциональном JavaScript

  • вторник, 21 марта 2017 г. в 03:14:24
https://habrahabr.ru/company/nixsolutions/blog/324342/
  • Программирование
  • JavaScript
  • Блог компании NIX Solutions


Предлагаем вашему вниманию переводной материал об использовании 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 превращает основной тип массива в полный функтор. Это делает функциональное программирование ещё более доступным.

Согласно классическим определениям функционального программирования, функтор удовлетворяет трём критериям:

  1. Содержит набор значений.
  2. Реализует функцию map для оперирования каждым элементом.
  3. Функция 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() в вашем приложении.