Предлагаем перевод статьи, которая позволит освежить свои знания по теме, а также будет полезна новичкам в 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
вы не будете менять состояние массива по мере вычислений. Каждый раз вы будете возвращать новый массив, а исходный останется нетронутым.Комментарии (32)
dregenor
17.03.2017 13:14последний пример с редюсом какой-то надуманный, там вполне хватило-бы .join('')
на мой вкус редюс лучше подходит для преобразования массивов в объект
var dict = ['Cat', 'Dog', 'Birg'].reduce((result, an, id) => { result[an] = id; return result;});
iShatokhin
17.03.2017 14:21+1Только ваш пример работать не будет, вы забыли инициализировать начальное значение.
var dict = ['Cat', 'Dog', 'Birg'].reduce((result, an, id) => { result[an] = id; return result;}, {});
dregenor
17.03.2017 23:46Каюсь, поспешил и забыл элементарно проверить ( в своем глазу бревна не увидел :)
vintage
18.03.2017 20:45var dict = {} ; [ 'Cat', 'Dog', 'Birg' ].forEach( ( an , id )=> dict[ an ] = id )
MrGobus
17.03.2017 16:11Не забывайте, что метод filter наверняка будет работать чуть медленнее, чем цикл for, пока браузеры и JS-движки не будут оптимизированы под новые методы работы с массивами.
Парой абзацев ниже
Не бойтесь начать использовать фильтрацию. В ES5 эта функциональность нативна и поддерживается почти везде.
АААААА, как быть то? Получается, не бояться использовать чтобы тормозило?Juma
17.03.2017 17:28Консоль Google Chrome выдает «Array.filter (native)»
Полагаю хоть немного, но все же оптимизированы.
Хотя можно и проверить какой способ и насколько быстрее.
vba
17.03.2017 17:21А разве в основе filter/map/reduce не лежат простые циклы?
Juma
17.03.2017 17:34Согласно документации там в основе лежат обычные циклы с кучей проверок, но нативные методы могут
должныбыть оптимизированы.vba
17.03.2017 17:44Не совсем понятно в чем суть оптимизации если результат все равно будет O(n). Разве сейчас это уже не нативные методы? Во всяком случае в chrome
Array.prototype.filter
очень похож на нативный.Large
17.03.2017 22:43Нативный метод может быть реализован на javascript или еще хуже — смеси c++ и javascript, сейчас почти все методы массива не оптимизированы на типизированных массивах. O(n) тоже бывает разным.
vba
18.03.2017 01:13Мне кажется это уже попахивает фанатизмом, конечно O(n) бывает разным, на разных поколениях машин. Давайте теперь не будем пользоваться умножением и делением, особенно внутри циклов, ведь это очень накладно. Если вы пишете компилятор C++ на JavaScript, то наверное вы что то делаете не так, а для повседневного использования, отрисовки списков или обработки асинхронных сообщений на стороне сервера вам должно за глаза хватить всех операций стоимостью O(n). Про корень зла не забыли?
Large
18.03.2017 01:20Я про внутреннюю реализацию в браузере, сейчас в v8 эти методы переводят на с++ и они будут инлайнится компилятором чтоб убрать расходы на переключение между с++ и js и повысить производительность. Про O(n) — сравните 100 n и n ** 2 при небольших n — или n и 10000 n, все имеет значение, для этого и используют комбинированные алгоритмы. У меня повседневное использование это счет и отрисовка графики, компилятор — нет, хотя не вижу в этом ничего дурного, но кодеки писали, не сказал бы, что мы что-то делаем не так.
vba
18.03.2017 02:16Неужели в счете и отрисовке вы используете filter/map/reduce? Хотя мы уходим немного в сторону. Вот смотрите кусочек Linq#Where кода аналога filter под Mono:
static IEnumerable<TSource> CreateWhereIterator<TSource> (IEnumerable<TSource> source, Func<TSource, bool> predicate) { foreach (TSource element in source) if (predicate (element)) yield return element; }
Там есть еще один такой же для массивов с циклом
for
. Filter версия должна быть очень схожа. Я не вижу разницы буть то написано даже на js или прямиком на c++. Метод прост как трусы за рубль двадцать. Вот что здесь оптимизировать, какие тут мега сложные проверки, которые нужно устранить?Large
18.03.2017 02:28А почему я их не должен использовать? Я и Foldable монады использую с их методом reduce там где это помогает читабельности. Оптимизация — это уже второй вопрос, где нужно можно и через цикл переписать, но обычно до этого не доходит, тем более на этом много не выиграешь.
В моно у вас компилятор перед выполнением все проверки сделает и выдаст вам идеальный машинный код, а в жс это все будет делать JIT ему прийдется многое угадывать и про ваш элемент и про ваш предикат. А если код который будет выполняться в компиляторе написан на смеси жс и с++ (да v8, это как раз компилятор с++ на жс и с++) то у вас будет теряться время на переход, так что не все так просто.
Quilin
20.03.2017 13:11Ну хотя метод и прост на первый взгляд, под использованием foreach (если это C#) спрятаны вызовы GetEnumerator, Next, Current — которые могут быть переопределены и сделаны вообще специально для того чтобы помогать вам разбираться с коллизиями в духе
var a = ['a'] a.filter(i => { a.push('b'); return Math.random() > 0.5 })
Трусы за рубль двадцать будут несколько проще таки. И оптимизировать тут есть что.
UnknownUser
17.03.2017 17:54Я правильно понимаю что
const threeLetterAnimals = animals
.filter(exactlyThree)
.map(capitalize)
.reduce(studlyCaps);
сначала создаст отфильтрованный массив, потом ещё дополнительно два раза по нему пробежится?
Если так, то выглядит, конечно, красиво, но как то не сильно производительно.
Ясное дело, это пример, но всё равно выглядит как то не очень.vba
17.03.2017 18:09Добро пожаловать в жестокий мир. Не совсем понятно почему это не сильно производительно, сложность тут линейная O(n). Но к счастью если вас заботит раздутие конечного автомата вы всегда можете использовать трансдьюсeры
UnknownUser
17.03.2017 22:01сложность тут линейная O(n)
Это не отменяет факта, что по массиву пробежимся два раза.
За трансдьюсеры спасибо, не знал про такое. Тогда да, можно красоту наводить.Large
17.03.2017 22:40оверхед самого цикла не большой потому разница между
for(const el of arr) {f(el); g(el);}
и
for(const el of arr) f(el); for(const el of arr) g(el);
не значительнаUnknownUser
18.03.2017 14:38Если массив большой, разница времени выполнения будет различаться в два раза. Если массив маленький, то не сильно страшно, конечно.
Но всё равно мне не нравится такое увеличение на пустом месте ради красоты. Наверное, опыт программирования микроконтроллеров даёт такие страшные побочные эффекты ))).Large
18.03.2017 14:48Для тежелых операций f, g разница не может быть в два раза, если операции очень дешевые, то оверхед может быть заметен и как раз и будет давать разницу в два раза.
Как правило, если проект большой, лучше писать красивее и понятнее, а уже потом думать о скорости. Часто узкие места появляются совсем не там где их ждешь.
Large
18.03.2017 14:59Просто в жс быстрый код — это обычно уродский код, а его не так просто поддерживать. Понятно, что стоит выделять критические места типа работы с многомерными массивами в библиотеки и их уже максимально оптимизировать. Но проект лучше держать в читабельном виде максимально долго.
xGromMx
17.03.2017 18:38+1Сколько можно уже мусолить эту тройку {map, filter, reduce} (первые 2 легко выводятся из свертки(катаморфизма) reduce)?
knotri
Это бред. Не выдумывайте свои определения
stardust_kid
Это перевод.
Quilin
А подскажите, какое бредовое определение выдумал автор оригинального поста?
Zenitchik
Под «встроенными» в русском языке понимаются отнюдь не «инлайновые».
Quilin
«Встроенная» никак не тянет на определение. Я согласен, впрочем, что обратный перевод скорее превращается в embedded.
Вопрос на засыпку — а как перевести «inline» на русский язык?
LFedorov
Я думаю в данном контексте вполне уместно было бы перевести как анонимная однострочная стрелочная ф-ция. По крайней мере такие определения встречаются довольно часто. Я не переводчик, но лично мне так было бы понятней.
jMas
Некоторое не переводится, например "инлайн стили" уже устоялись, было бы странно переводить как "встроенные стили". Если в контексте статьи и конкретной фарзы, то перевод слова инлайн избыточен, поэтому достаточно было написать "стрелочаня функция".