filter
— вбудований метод ітерації масивом, який приймає предикат, викликаний для кожного з його значень і повертає підмножину значень, що є істинними.
Розглянемо наведене визначення детальніше:
- Вбудований означає, що метод є частиною мови: вам не потрібно додавати будь-яку бібліотеку для отримання доступу до функціоналу.
-
Метод ітерації означає виконання для кожного елементу масиву. Прикладами таких методів можуть слугувати
map
таreduce
. - Предикат — функція, що повертає булеве значення.
-
Істинне значення — будь-яке значення, що може розглядатися як
true
. Майже всі значення можуть бутиtrue
за виняткомundefined
,null
,false
,0
,NaN
, або""
(empty string).
Аби побачити filter
у дії, спершу поглянемо на масив ресторанів.
const restaurants = [
{
name: "Dan's Hamburgers",
price: 'Cheap',
cuisine: 'Burger',
},
{
name: "Austin's Pizza",
price: 'Cheap',
cuisine: 'Pizza',
},
{
name: "Via 313",
price: 'Moderate',
cuisine: 'Pizza',
},
{
name: "Bufalina",
price: 'Expensive',
cuisine: 'Pizza',
},
{
name: "P. Terry's",
price: 'Cheap',
cuisine: 'Burger',
},
{
name: "Hopdoddy",
price: 'Expensive',
cuisine: 'Burger',
},
{
name: "Whataburger",
price: 'Moderate',
cuisine: 'Burger',
},
{
name: "Chuy's",
cuisine: 'Tex-Mex',
price: 'Moderate',
},
{
name: "Taquerias Arandina",
cuisine: 'Tex-Mex',
price: 'Cheap',
},
{
name: "El Alma",
cuisine: 'Tex-Mex',
price: 'Expensive',
},
{
name: "Maudie's",
cuisine: 'Tex-Mex',
price: 'Moderate',
},
];
Тут багато інформації. Я зараз у настрої з'їсти бургер, тому відфільтруємо трохи масив.
const isBurger = ({cuisine}) => cuisine === 'Burger';
const burgerJoints = restaurants.filter(isBurger);
isBurger
— предикат, а burgerJoints
— новий масив, що є підмножиною ресторанів. Важливо відмітити, що масив ресторанів залишається незмінним після застосування filter
.
Нижче наведено простий приклад відображення двох списків: один — початковий масив restaurants
, інший — відфільтрований burgerJoints
масив.
Заперечення предикатів
Для кожного предикату існує рівний та протилежний предикат.
Як вже було зазначено, предикат — функція, що повертає булеве значення. Зважаючи на те, що таких значень два, значення предиката легко «перевернути».
Декілька годин пройшло з того часу, як я з'їв свій бургер, і тепер я голодний знову. Цього разу я хочу отримати результат фільтрації без бургерів аби скуштувати щось нове. Один з варіантів — написати новий предикат isNotBurger
.
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isNotBurger = ({cuisine}) => cuisine !== 'Burger';
Однак, подивіться на схожість двох предикатів. Цей код містить повторення. Інший спосіб — викликати предикат isBurger
та змінити результат на протилежний.
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isNotBurger = restaurant => !isBurger(restaurant);
Вже краще. Якщо критерій пошуку змінюється на протилежний, вам необхідно змінити логіку лише в одному місці. Однак, якщо існує декілька предикатів, які б ми хотіли заперечити. Така необхідність може виникати часто, тому гарною ідеєю буде написати функцію negate
.
const negate = predicate => function() {
return !predicate.apply(null, arguments);
}
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isNotBurger = negate(isBurger);
const isPizza = ({cuisine}) => cuisine === 'Pizza';
const isNotPizza = negate(isPizza);
У вас можуть виникнути питання.
Що таке .apply
?
Метод.apply
приймає функцію з заданимиthis
значеннями та аргументами у вигляді масиву (чи масивоподібного об'єкту).
Що таке аргументи?
Об'єктarguments
— локальна змінна, доступна у всіх функціях (за винятком стрілкових). Ви можете посилатися на аргументи функції всередині функції, використовуючи об'єктarguments
.
Навіщо повертати стару добру функцію замість нової стрілкової функції?
У даному випадку повернення традиційної функції є необхідним, тому що аргументи доступні лише у таких функціях.
(Але ви можете використовувати negate
зі стрілковою функцією, використовуючи параметри rest
)
Повернення предикатів
На прикладі нашої negate
легко впевнитись у тому, що функція у JavaScript з легкістю повертає нову. Це може бути корисним для написання «творців предикатів». Наприклад, звернемося знову до вже знайомих предикатів isBurger
та isPizza
.
const isBurger = ({cuisine}) => cuisine === 'Burger';
const isPizza = ({cuisine}) => cuisine === 'Pizza';
Наведені предикати поділяють спільну логіку: вони відрізняються лише у порівняннях. Саме тому ми можемо огорнути загальну логіку функцією isCuisine
.
const isCuisine = comparison => ({cuisine}) => cuisine === comparison;
const isBurger = isCuisine('Burger');
const isPizza = isCuisine('Pizza');
Усе чудово! Що буде, коли ми захочемо перевірити ціну?
const isPrice = comparison => ({price}) => price === comparison;
const isCheap = isPrice('Cheap');
const isExpensive = isPrice('Expensive');
Тепер isCheap
та isExpensive
, а також isPizza
та isBurger
не повторюють код, але isPrice
та isCuisine
поділяють їх логіку! На щастя, немає обмежень щодо кількості функцій, що повертаються.
const isKeyEqualToValue = key => value => object => object[key] === value;
// це можна переписати
const isCuisine = isKeyEqualToValue('cuisine');
const isPrice = isKeyEqualToValue('price');
// це не треба змінювати
const isBurger = isCuisine('Burger');
const isPizza = isCuisine('Pizza');
const isCheap = isPrice('Cheap');
const isExpensive = isPrice('Expensive');
Як на мене, продемонстровано красу стрілкових функцій. В один рядок ви можете елегантно створити вкладену функцію. isKeyEqualToValue
— функція, що повертає функцію isPrice
, яка у свою чергу повертає функцію isCheap
.
Тепер ви бачите як легко можна створити декілька відфільтрованих списків з початкового масиву restaurants
.
Поєднання предикатів
Тепер ви маєте змогу відфільтрувати масив за наявністю бургерів чи за ціною... але що, якщо ви хочете дешеві бургери? Як варіант, можна зв'язати обидва фільтри.
const cheapBurgers = restaurants.filter(isCheap).filter(isBurger);
Іншим варіантом є «складання» двох предикатів в один.
const isCheapBurger = restaurant => isCheap(restaurant) && isBurger(restaurant);
const isCheapPizza = restaurant => isCheap(restaurant) && isPizza(restaurant);
Погляньте на весь цей повторюваний код. Його ми можемо об'єднати у нову функцію.
const both = (predicate1, predicate2) => value =>
predicate1(value) && predicate2(value);
const isCheapBurger = both(isCheap, isBurger);
const isCheapPizza = both(isCheap, isPizza);
const cheapBurgers = restaurants.filter(isCheapBurger);
const cheapPizza = restaurants.filter(isCheapPizza);
А якщо ви не проти піци й гамбургерів?
const either = (predicate1, predicate2) => value =>
predicate1(value) || predicate2(value);
const isDelicious = either(isBurger, isPizza);
const deliciousFood = restaurants.filter(isDelicious);
Ми зробили крок у правильному напрямку, але якщо у вас є понад дві страви на вибір? Тож підхід є не дуже масштабованим. Існує два вбудованих методи масивів, що приходять на допомогу. .every
та .some
є методами предикатів, що також приймають їх. .every
перевіряє чи кожен елемент масиву відповідає предикату, у той час як .some
перевіряє чи є елемент масиву, що відповідає предикату.
const isDelicious = restaurant =>
[isPizza, isBurger, isBbq].some(predicate => predicate(restaurant));
const isCheapAndDelicious = restaurant =>
[isDelicious, isCheap].every(predicate => predicate(restaurant));
Звичайно, додамо трохи необхідної абстракції.
const isEvery = predicates => value =>
predicates.every(predicate => predicate(value));
const isAny = predicates => value =>
predicates.some(predicate => predicate(value));
const isDelicious = isAny([isBurger, isPizza, isBbq]);
const isCheapAndDelicious = isEvery([isCheap, isDelicious]);
isEvery
та isAny
обидва приймають масив предикатів та повертають єдиний предикат.
Оскільки такі предикати запросто створюються функціями вищого порядку, їх створення та застосування на основі взаємодії з користувачем не повинно викликати труднощів. Для засвоєння викладеного, нижче наведено приклад застосунку, що шукає ресторани на основі застосованих фільтрів-кнопок.
Ще немає коментарів