Стрелкови функции
Проблем
Цяла функция за едно сравнение
Поръчките трябва да се подредят по сума.
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));4045student е обект от тип 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));Стъпка по стъпка, с думи
- Стрелката е обект от тип function, а student сочи към него. Тялото ѝ още не се изпълнява.
- Второ правило, втори обект. Нито един клас не е написан.
- student(50) подава 50 на функцията: price е 50, а price * 0.8 дава 40.
- 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.67andThen приема две функции и връща трета: тя вика първата, а резултатът ѝ подава на втората.
По-точно, за любопитните
Действието все пак връща нещо: 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));bigAndMaria дава false, а bigOrMaria дава true, защото е на Мария. Поръчката на Георги е 80 €, голяма е, а ! обръща отговора: false.Виж го
Масивите приемат функции
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.75b.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}`));reversed обръща цялата функция, която получава, а тя е „по оценка, после по име“. Затова се обръщат и двете: оценките са от най-високата, а Петър е преди Иван. Ако искаш само оценките наобратно, разменяш a и b само в сравнението на оценките, както в картата преди тази.Пробвай
Намери грешката
Избери реда с грешката.
total е const, а стрелката се опитва да я промени:
TypeError: Assignment to constant variable.Грешката излиза на ред 3, но причината е на ред 2: стойността на const не се сменя. С let total = 0; програмата печата 127.
Тук JavaScript е различен от Java. Там ламбдата получава копие на стойността и не може да променя локалната променлива изобщо. В JavaScript стрелката вижда самата променлива: може да я чете и, ако е let, да я променя. За сбор обаче има по-ясен начин, reduce, в следващия урок.
По-точно, за любопитните
Стрелката вижда самата променлива, не копие. Ако променливата се смени след стрелката, при следващото извикване тя вижда новата стойност:
let rate = 0.2;const discount = (price) => price * (1 - rate);console.log(discount(100));
rate = 0.5;console.log(discount(100));8050В 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 €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. Пробвай всеки на няколко низа.
Тест