Статьи

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

Эта статья была рецензирована Дэном Принсом , Вилданом Софтиком и Джоан Инн . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Ной фильтрует животных, ожидающих посадки в ковчег

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

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

Фильтрация для ограничения набора данных

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

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

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

Простой пример такого рода проблем, которые могут выиграть от фильтрации, — это ограничение массива строк только строками, состоящими из трех символов. Это не сложная проблема, которую можно решить, и мы можем сделать это довольно просто, используя ванильный JavaScript 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"] 

Здесь мы определяем массив, содержащий три строки, и создаем пустой массив, в котором мы можем хранить только строки, содержащие только три символа. Мы определяем переменную count для использования в цикле for при выполнении итерации по массиву. Каждый раз, когда мы встречаем строку, содержащую ровно три символа, мы помещаем ее в наш новый пустой массив. И как только мы закончили, мы просто регистрируем результат.

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

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

Технически нет ничего плохого в том, как мы это сделали, но наличие метода filter в Array позволяет нам сделать наш код намного чище и понятнее. Вот пример того, как мы могли бы сделать то же самое, используя метод filter :

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

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

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

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

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

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

Наш код может быть еще более кратким, если мы воспользуемся объявлениями const и анонимными встроенными функциями стрелок. Это функции 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"] 

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

Быстрый обзор карты и уменьшить

Фильтрация работает рука об руку с двумя другими функциональными методами Array из ES5, map и lower. И благодаря возможности цепочки методов в JavaScript вы можете использовать эту комбинацию для создания очень чистого кода, который выполняет некоторые довольно сложные функции.

В качестве быстрого напоминания метод 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 , вы можете проверить мою предыдущую статью об использовании map и Reduce в функциональном JavaScript .

Сцепление карты, уменьшение и фильтрация

В качестве очень простого примера того, что возможно, давайте представим, что вы хотели взять массив строк и вернуть одну строку, содержащую только трехбуквенные строки из оригинала, но вы хотели отформатировать полученную строку в 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 . Использование 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 пока браузеры и механизмы JavaScript не оптимизируются для новых методов Array ( jsPerf ).

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

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

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

Согласен? Не согласен? Комментарии приветствуются ниже.