...

пятница, 17 марта 2017 г.

[Перевод] Фильтрация и создание цепочек в функциональном JavaScript

Предлагаем перевод статьи, которая позволит кому-то освежить свои знания по теме, а также будет полезна новичкам в JavaScript, пока ещё осваивающим этот язык.

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

Хотя JavaScript поддерживает методики функционального программирования, он не оптимизирован для полноценного использования этой парадигмы, как Haskell или Scala. Не обязательно добиваться того, чтобы ваши JS-программы полностью соответствовали концепциям функционального программирования. Но их применение помогает поддерживать чистоту кода и концентрироваться на создании архитектуры, которая легко тестируется и может использоваться в нескольких проектах.

Фильтрация для ограничения датасетов


С появлением ES5 массивы в JS унаследовали несколько методов, делающих функциональное программирование ещё удобнее. Теперь массивы нативно поддерживают map, reduce и filter. Каждый метод проходит по всем элементам массива, и выполняет анализ без использования циклов и изменения локальных состояний. Результат может быть возвращён для немедленного использования, или оставлен для последующей обработки.

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

Использование цикла для демонстрации фильтрации


Пример проблемы, решить которую поможет фильтрация — ограничение массива, содержащего строковые значения, только теми, что состоят из трёх символов. Задача не сложная, и решить её можно довольно искусно с помощью ванильных JS-циклов for, без использования filter. Например:
var animals = ["cat","dog","fish"];
var threeLetterAnimals = [];
for (let count = 0; count < animals.length; count++){
  if (animals[count].length === 3) {
    threeLetterAnimals.push(animals[count]);
  }
}
console.log(threeLetterAnimals); // ["cat", "dog"]

Определили массив, содержащий три строковых значения. Создали пустой массив для хранения только строковых из трёх символов. Определили переменную-счётчик для цикла for, используемую по мере итерирования массива. Каждый раз, когда цикл находит строковое значение из трёх символов, он помещает его во второй массив. По завершении работы результат журналируется.

Ничто не мешает менять исходный массив в цикле. Но если мы это сделаем, то потеряем исходные значения. Лучше создать новый массив, а исходный не трогать.

Использование метода Filter


Предыдущее решение технически корректно. Но использование метода filter позволяет сделать код гораздо чище и проще. Например:
var animals = ["cat","dog","fish"];
var threeLetterAnimals = animals.filter(function(animal) {
  return animal.length === 3;
});
console.log(threeLetterAnimals); // ["cat", "dog"]

Здесь мы тоже начали с переменной, содержащей исходный массив. Определили новую переменную для массива, куда будем класть строковые из трёх символов. Но применив метод filter, мы напрямую связали результаты фильтрации со вторым массивом. Передаём filter анонимной встроенной (in-line) функции, возвращающей true, если длина оперируемого значения равна трём.

Метод filter работает так: проходит по каждому элементу массива и применяет к нему тестовую функцию (test function). Если функция возвращает true, то метод filter возвращает массив, содержащий этот элемент. Другие элементы пропускаются.

Код получился гораздо чище. Даже не зная заранее, что делает filter, вы из кода можете понять его принцип действия.

Чистота кода — один из приятных побочных продуктов функционального программирования. Это следствие ограничения преобразования внешних переменных из функций и необходимости хранить меньше локальных состояний. Переменная count и разные состояния, принимаемые массивом threeLetterAnimals при прохождении циклов по исходному массиву, это дополнительные состояния, которые надо отслеживать. Метод filter избавил нас от цикла и переменной count. И мы не меняем многократно значение для нового массива, как в первом случае. Мы определили его один раз и связали со значением, получаемым в результате применения условия filter к исходному массиву.

Другие способы форматирования Filter


Можно написать ещё короче. Воспользуемся объявлениями const и анонимными встроенными стрелочными функциями (inline arrow functions). Это благодаря EcmaScript 6 (ES6), который нативно поддерживается большинством браузеров и JavaScript-движков.
const animals = ["cat","dog","fish"];
const threeLetterAnimals = animals.filter(item => item.length === 3);
console.log(threeLetterAnimals); // ["cat", "dog"]

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

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

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

const animals = ["cat","dog","fish"];
function exactlyThree(word) {
  return word.length === 3;
}
const threeLetterAnimals = animals.filter(exactlyThree);
console.log(threeLetterAnimals); // ["cat", "dog"]

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

Быстрый обзор Map и Reduce


Фильтрация работает рука об руку с двумя другими функциональными методами ES5 — map и reduce. Создавая цепочки методов, можно использовать эту комбинацию для написания очень чистого кода, выполняющего довольно сложные функции.

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

const animals = ["cat","dog","fish"];
const lengths = animals.map(getLength);
function getLength(word) {
  return word.length;
}
console.log(lengths); //[3, 3, 4]

Метод reduce проходит по массиву и выполняет ряд операций. Промежуточный результат каждой из них передаёт в сумматор. По завершении обработки массива метод выдаёт финальный результат. В нашем случае можно использовать второй аргумент для начальной установки сумматора в 0.
const animals = ["cat","dog","fish"];
const total = animals.reduce(addLength, 0);
function addLength(sum, word) {
  return sum + word.length;
}
console.log(total); //10

Все три метода оставляют нетронутым исходный массив, в соответствии с практикой функционального программирования.

Создание цепочек из Map, Reduce и Filter


Рассмотрим простейший пример. Допустим, вам нужно взять массив из строковых значений, и вернуть одно, состоящее из трёх символов. Но при этом отформатировать его в стиле StudlyCaps. Без map, reduce и filter это будет выглядеть примерно так:
const animals = ["cat","dog","fish"];
let threeLetterAnimalsArray = [];
let threeLetterAnimals;
let item;
for (let count = 0; count < animals.length; count++){
  item = animals[count];
  if (item.length === 3) {
    item = item.charAt(0).toUpperCase() + item.slice(1);
    threeLetterAnimalsArray.push(item);
  }
}
threeLetterAnimals = threeLetterAnimalsArray.join("");
console.log(threeLetterAnimals); // "CatDog"

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

Объявлять целевой пустой массив можно с помощью let или const.

Создадим чистые функции, берущие и возвращающие строковые значения. Затем используем их в цепочках методов map, reduce и filter, передавая результаты от одного к другому:

const animals = ["cat","dog","fish"];
function studlyCaps(words, word) {
  return words + word;
}
function exactlyThree(word) {
  return (word.length === 3);
}
function capitalize(word) {
  return word.charAt(0).toUpperCase() + word.slice(1);
}
const threeLetterAnimals = animals
  .filter(exactlyThree)
  .map(capitalize)
  .reduce(studlyCaps);
console.log(threeLetterAnimals); // "CatDog"

Три чистые функции: studlyCaps, exactlyThree и capitalize. Можно передавать их напрямую в map, reduce и filter в пределах одной неразрывной цепочки. Сначала с помощью exactlyThree фильтруем исходный массив. Передаём результат в capitalize. А уже её результат обрабатываем с помощью studlyCaps. Финальный результат присваиваем напрямую переменной threeLetterAnimals. Без циклов и промежуточных состояний, не трогая исходный массив.

Получили очень понятный и легко тестируемый код. Чистые функции могут быть использованы в других контекстах или преобразованы.

Фильтрация и производительность


Не забывайте, что метод filter наверняка будет работать чуть медленнее, чем цикл for, пока браузеры и JS-движки не будут оптимизированы под новые методы работы с массивами (jsPerf).

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

Если же окажется, что filter в реальной ситуации работает значительно медленнее цикла, если это влияет на пользователей, то вы знаете, где и как можно оптимизировать. А по мере допиливания JS-движков производительность будет только расти.

Не бойтесь начать использовать фильтрацию. В ES5 эта функциональность нативна и поддерживается почти везде. Ваш код будет чище и проще в сопровождении. Благодаря методу filter вы не будете менять состояние массива по мере вычислений. Каждый раз вы будете возвращать новый массив, а исходный останется нетронутым.

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

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий