Към съдържанието
wannacode

Стрелкови функции

Проблем

Цяла функция за едно сравнение

Поръчките трябва да се подредят по сума.

Order
class Order {
constructor(customer, total) {
this.customer = customer;
this.total = total;
}
}
const orders = [
new Order("Мария", 75.5),
new Order("Георги", 12.9),
new Order("Елена", 40),
];
function byTotal(a, b) {
return a.total - b.total;
}
orders.sort(byTotal);
for (const o of orders) {
console.log(`${o.customer}: ${o.total.toFixed(2)} €`);
}
Изход
Георги: 12.90 €
Елена: 40.00 €
Мария: 75.50 €

sort приема функция за сравнение. Тя получава две поръчки и връща отрицателно число, ако първата трябва да е преди втората, положително, ако е след нея, и 0, ако са равни. a.total - b.total прави точно това.

Работи. Но за едно сравнение пишеш цяла функция с име, далеч от мястото, където я ползваш. А тя се вика само тук. Искаш да подадеш само правилото.

Идеята

Стрелкова функция: само правилото

Същото подреждане, на един ред:

orders.sort((a, b) => a.total - b.total);

(a, b) => a.total - b.total се казва стрелкова функция (на английски arrow function). В другите езици същото се казва ламбда. Отляво на => са параметрите, отдясно е тялото. Това е функцията byTotal без име, без function и без return.

Видовете запис:

  • Един параметър може да е без скоби: o => o.total > 50. Обикновено скобите се пишат: (o) => .... Без параметри е () => ....
  • Тяло с няколко реда е във { } и тогава return е задължителен.
const byTotal = (a, b) => a.total - b.total;
const byTotalLong = (a, b) => {
const diff = a.total - b.total;
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
};
const guest = () => new Order("Гост", 0);
orders.sort(byTotalLong);
for (const o of orders) {
console.log(`${o.customer}: ${o.total.toFixed(2)} €`);
}
console.log(guest().customer);
Изход
Георги: 12.90 €
Елена: 40.00 €
Мария: 75.50 €
Гост

Стрелковата функция е стойност: може да я сложиш в променлива и да я подадеш по-късно.

По-точно, за любопитните

Ако тялото е обект, той е в кръгли скоби. Без тях { се чете като начало на тяло, и функцията връща undefined:

const guest = () => ({ customer: "Гост" });
const broken = () => { customer: "Гост" };
console.log(guest().customer);
console.log(broken());
Изход
Гост
undefined

Идеята

Функцията е стойност

Откъде sort знае, че (a, b) => ... е функцията за сравнение? Не му трябва да знае нищо повече. В JavaScript функцията е стойност като число или текст: може да я сложиш в променлива, да я подадеш на друга функция и да я извикаш със скоби.

const student = (price) => price * 0.8;
const minusFive = (price) => price - 5;
console.log(student(50));
console.log(minusFive(50));
Изход
40
45

student е обект от тип function: typeof student е "function". Викаш го като всяка функция: student(50). Две правила за цена, нито един клас.

Java тук има нужда от функционален интерфейс: интерфейс с един метод, който казва какъв е видът на правилото. JavaScript не пише типове и няма интерфейси, затова такова нещо не трябва. Всяка функция може да се подаде и извика.

const student = (price) => price * 0.8;
const minusFive = (price) => price - 5;
console.log(student(50));
console.log(minusFive(50));
Стъпка по стъпка, с думи
  1. Стрелката е обект от тип function, а student сочи към него. Тялото ѝ още не се изпълнява.
  2. Второ правило, втори обект. Нито един клас не е написан.
  3. student(50) подава 50 на функцията: price е 50, а price * 0.8 дава 40.
  4. minusFive(50): същото число, друго правило, 45.
По-точно, за любопитните

Стрелката взима this от мястото, където е написана, както ламбдата в Java. Функцията, написана с function, има свое this, а когато forEach я вика, то е undefined:

class Cart {
constructor(owner) {
this.owner = owner;
this.items = ["кафе", "мед"];
}
print() {
this.items.forEach((item) => console.log(`${this.owner}: ${item}`));
}
printOld() {
this.items.forEach(function (item) {
console.log(`${this.owner}: ${item}`);
});
}
}
const cart = new Cart("Мария");
cart.print();
cart.printOld();
Изход
Мария: кафе
Мария: мед
TypeError: Cannot read properties of undefined (reading 'owner')

Затова правило в метод на клас пиши като стрелка.

Идеята

Четирите вида правила

Типът на всяко правило е function, но видовете правила са четири. Добре е да ги разпознаваш, защото функциите, които приемат правила, искат определен вид:

  • условие: приема стойност, връща true или false. „Голяма ли е поръчката?“
  • превръщане: приема едно, връща друго. Поръчка в етикет.
  • действие: приема стойност и прави нещо с нея: отпечатва я, записва я. Резултатът му не се ползва.
  • доставчик: не приема нищо, връща нов обект, когато поискаш.
const big = (o) => o.total >= 50;
const label = (o) => o.customer.toUpperCase();
const show = (text) => console.log("-> " + text);
const guest = () => new Order("Гост", 0);
const order = new Order("Мария", 75.5);
console.log(big(order));
show(label(order));
console.log(guest().customer);
Изход
true
-> МАРИЯ
Гост

В Java правилата са обекти с методи, които ги сглобяват: условията се свързват с and, or и negate („не“), а функциите се нареждат една след друга с andThen. В JavaScript такива методи няма. Новото правило е нова функция, която вика старите: (o) => big(o) && maria(o). Две функции една след друга също се сглобяват с функция:

const withVat = (price) => price * 1.2;
const rounded = (price) => Math.round(price * 100) / 100;
const andThen = (f, g) => (x) => g(f(x));
const finalPrice = andThen(withVat, rounded);
console.log(finalPrice(10.555));
Изход
12.67

andThen приема две функции и връща трета: тя вика първата, а резултатът ѝ подава на втората.

По-точно, за любопитните

Действието все пак връща нещо: undefined, защото това е резултатът на console.log. console.log(show("кафе")) отпечатва „-> кафе“ и после undefined.

Пробвай

Сглобени условия

Какво ще отпечата програмата?

const big = (o) => o.total >= 50;
const maria = (o) => o.customer === "Мария";
const bigAndMaria = (o) => big(o) && maria(o);
const bigOrMaria = (o) => big(o) || maria(o);
const notBig = (o) => !big(o);
const small = new Order("Мария", 30);
const large = new Order("Георги", 80);
console.log(bigAndMaria(small));
console.log(bigOrMaria(small));
console.log(notBig(large));
Отговор

Виж го

Масивите приемат функции

Масивите имат методи, които приемат правило вместо цикъл:
let cart = ["хляб", "", "мляко", " ", "кафе"];
cart = cart.filter((item) => item.trim() !== "");
cart.forEach((item) => console.log(item));
const customers = [
{ name: "Мария", city: "Пловдив" },
{ name: "Георги", city: "София" },
{ name: "Елена", city: "Пловдив" },
];
const byCity = Map.groupBy(customers, (c) => c.city);
byCity.forEach((list, city) => console.log(`${city}: ${list.map((c) => c.name).join(", ")}`));
Изход
хляб
мляко
кафе
Пловдив: Мария, Елена
София: Георги
  • filter(условие) прави нов масив само с елементите, за които условието е вярно. Старият не се променя: тук cart е let и просто сочи новия. Метод, който маха елементи от самия масив по условие, няма.
  • forEach(действие) прави действието с всеки елемент.
  • Map.groupBy(масив, функция) групира: функцията казва ключа на всеки елемент, а резултатът е Map с масив от елементите за всеки ключ.
  • forEach на Map приема функция с два параметъра, но в обратен ред: първо стойността, после ключа.

{ name: "Мария", city: "Пловдив" } е обект без клас: само полета. Map пази ключовете в реда на добавяне, затова Пловдив е преди София.

Идеята

Функция по име, без скоби

Често стрелката само подава параметъра си на готова функция: (s) => Number(s). Тогава може да подадеш самата функция, по име и без скоби:
const names = ["мария", "георги"];
names.forEach((name) => console.log(name));
const upper = (s) => s.toUpperCase();
const parse = Number;
console.log(upper("елена"));
console.log(parse("42") + 1);
console.log(["1", "2", "3"].map(Number));
console.log(["1", "2", "3"].map(parseInt));
Изход
мария
георги
ЕЛЕНА
43
[ 1, 2, 3 ]
[ 1, NaN, NaN ]

Java за това има отделен запис, препратка към метод с ::. В JavaScript е достатъчно името, но с една уговорка: функцията получава всичко, което ѝ подава викащият. map и forEach подават елемента, номера му и целия масив. Number гледа само първото и работи. parseInt приема втори параметър, основата на бройната система, и получава номера на елемента: parseInt("2", 1) е NaN. Същото става с console.log, който печата всичко, което получи:

["мария", "георги"].forEach(console.log);
Изход
мария 0 [ 'мария', 'георги' ]
георги 1 [ 'мария', 'георги' ]

Метод на параметъра, като toUpperCase, няма кратък запис. String.prototype.toUpperCase не може да се подаде направо: програмата спира с TypeError. Пише се (s) => s.toUpperCase().

Затова по име подавай само функции, за които знаеш какви параметри приемат. Иначе пиши стрелка.

Виж го

Подреждане по няколко полета

Функцията за сравнение може да провери няколко полета едно след друго. Ако първото сравнение даде 0, || минава към следващото: 0 в JavaScript е „лъжа“.

Учениците са подредени по оценка, от най-високата, а при равни оценки по име:

const students = [
new Student("Иван", 5.5),
new Student("Ана", 6),
new Student("Петър", 5.5),
new Student("Борис", 4.75),
];
students.sort((a, b) => b.grade - a.grade || a.name.localeCompare(b.name));
Изход
Ана 6
Иван 5.5
Петър 5.5
Борис 4.75

b.grade - a.grade поставя по-високата оценка отпред. При равни оценки a.name.localeCompare(b.name) сравнява имената по азбука. Чете се отляво надясно: „по оценка наобратно, а при равенство по име“.

По-точно, за любопитните
  • Без функция sort превръща елементите в текст и сравнява текста. За числа това дава грешен ред:
console.log([10, 9, 1].sort());
console.log([10, 9, 1].sort((a, b) => a - b));
const prices = [40, 12, 75];
const sorted = prices.toSorted((a, b) => a - b);
console.log(sorted, prices);
Изход
[ 1, 10, 9 ]
[ 1, 9, 10 ]
[ 12, 40, 75 ] [ 40, 12, 75 ]
  • sort подрежда самия масив. toSorted връща нов подреден масив, а старият остава какъвто е.

Пробвай

Какво обръща reversed?

Тук reversed приема функция за сравнение и връща нова, която разменя a и b.

Какво ще отпечата програмата?

const students = [
new Student("Иван", 5.5),
new Student("Ана", 6),
new Student("Петър", 5.5),
new Student("Борис", 4.75),
];
const byGradeThenName = (a, b) => a.grade - b.grade || a.name.localeCompare(b.name);
const reversed = (compare) => (a, b) => compare(b, a);
students.sort(reversed(byGradeThenName));
students.forEach((s) => console.log(`${s.name} ${s.grade}`));
Отговор

Пробвай

Намери грешката

Програмата трябва да отпечата сбора на цените. Спира с грешка още при първата цена. На кой ред е причината?

Избери реда с грешката.

По-точно, за любопитните

Стрелката вижда самата променлива, не копие. Ако променливата се смени след стрелката, при следващото извикване тя вижда новата стойност:

let rate = 0.2;
const discount = (price) => price * (1 - rate);
console.log(discount(100));
rate = 0.5;
console.log(discount(100));
Изход
80
50

В Java това е забранено точно заради такива изненади. Ако правилото трябва да помни стойност, подай му я като параметър.

Упражнение

Правила като стойности

Реши задачите в редактора си.

1. Подреждане по няколко полета. Клас Product с име, категория и цена. Подреди продуктите по категория, в една категория от най-скъпия, а при равна цена по име, с едно sort и функция за сравнение с ||. Продуктите са: мишка, аксесоари, 25 €; лаптоп, компютри, 1299 €; клавиатура, аксесоари, 49,90 €; таблет, компютри, 399 €; слушалки, аксесоари, 49,90 €.

Изход
Аксесоари | Клавиатура | 49.90 €
Аксесоари | Слушалки | 49.90 €
Аксесоари | Мишка | 25.00 €
Компютри | Лаптоп | 1299.00 €
Компютри | Таблет | 399.00 €

2. Филтър на поръчки. Добави към Order град. Напиши функция select(orders, rule), която връща нов списък само с поръчките, за които правилото е вярно. Направи две правила, sofia и big (поне 50 €), и отпечатай поръчките, за които са верни и двете, тези извън София и тези „в София или поне 100 €“. Всяко от трите правила е стрелка, която вика sofia и big. Поръчките: Мария, София, 75,50 €; Георги, Варна, 12,90 €; Елена, София, 40 €; Димитър, Пловдив, 120 €.

Изход
София, над 50 €:
Мария, София: 75.50 €
Извън София:
Георги, Варна: 12.90 €
Димитър, Пловдив: 120.00 €
Безплатна доставка (София или над 100 €):
Мария, София: 75.50 €
Елена, София: 40.00 €
Димитър, Пловдив: 120.00 €
3. Отстъпки една след друга. Направи три отстъпки като стрелки: студентска (−10%), за Черния петък (−20 €) и „не под нула“. Напиши andThen(f, g), която връща функция: първо f, после g. С нея свържи отстъпките в два реда: студентската първо и петъчната първо. Пробвай с 120 € и с 15 €. Редът има ли значение?
Изход
120.00 € -> студент, после петък: 88.00 €; петък, после студент: 90.00 €
15.00 € -> студент, после петък: 0.00 €; петък, после студент: 0.00 €

Още задачи

  • Чистене на списък. От масив с имейли махни празните и тези без @ с един filter, после отпечатай останалите с forEach.
  • Ученици по клас. Групирай ученици по клас ("10А", "10Б") с Map.groupBy и отпечатай всеки клас с имената в него.
  • Валидатори. Направи обект с три валидатора като стрелки: непразен текст, само цифри (/^\d+$/.test(value)), дължина до 10. Пробвай всеки на няколко низа.

Тест

Провери се

1. Коя е стрелкова функция, която връща сбора на две цени?
2. Как се сглобява условието „голяма и на Мария“ от функциите big и maria?
3. Какво връща ["1", "2", "3"].map(parseInt)?
4. Как подрежда students.sort((a, b) => b.grade - a.grade || a.name.localeCompare(b.name))?
5. const total = 0; prices.forEach((p) => (total += p)); Какво става?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·