Методите на масива
Проблем
Отчет в цикъл
Поръчките вече имат и град:
class Order { constructor(customer, city, total) { this.customer = customer; this.city = city; this.total = total; }}const orders = [ new Order("Мария", "София", 75.5), new Order("Георги", "Варна", 12.9), new Order("Елена", "София", 40), new Order("Димитър", "Пловдив", 120), new Order("Ана", "Варна", 64),];Искаш клиентите с поръчки поне 50 €, подредени по азбучен ред:
const result = [];for (const o of orders) { if (o.total >= 50) { result.push(o.customer); }}result.sort();console.log(result);[ 'Ана', 'Димитър', 'Мария' ]Осем реда, а въпросът е един. Кодът казва как: направи празен списък, обиколи, провери, добави, подреди. За да разбереш какво прави, трябва да го изпълниш наум. Добавиш ли „само първите трима“ или „без повторения“, цикълът ще расте.
Идеята
Верига от стъпки
Същият отчет, написан като стъпки:
const result = orders .filter((o) => o.total >= 50) .map((o) => o.customer) .toSorted();console.log(result);[ 'Ана', 'Димитър', 'Мария' ]Чете се отгоре надолу: вземи поръчките, остави тези поне за 50 €, вземи клиента, подреди. Всяка стъпка е метод на масива и приема стрелка от предишния урок:
filter(условие)оставя само елементите, за които условието е вярно;map(функция)превръща всеки елемент в друг: отOrderправи текст;toSorted()подрежда по азбука, аtoSorted(функция за сравнение)по твоето правило.
Всеки метод връща нов масив, а следващият се вика върху него. Затова стъпките се нареждат във верига. Масивът orders не се променя.
По-точно, за любопитните
- Старият
sortподрежда самия масив и връща него.toSortedе по-новият и връща нов масив. Защо разликата е важна, ще видиш в картата „Намери грешката“. - Всяка стъпка минава целия масив, преди да започне следващата. Конвейер, по който елементите минават един по един, има при итератора. За него е следващата карта.
Виж го
Какво минава през филтъра
values(). Итераторът има същите filter и map, но е конвейер: дава стойностите една по една, когато някой ги поиска. toArray() ги събира в масив.const prices = [12, 75, 40, 130, 58];const labels = prices .values() .filter((p) => p >= 50) .map((p) => `${p} €`) .toArray();console.log(labels);Стъпка по стъпка, с думи
- Пет цени. values() дава итератор: път, по който още нищо не е минало.
- 12 не минава p >= 50 и отпада. До map не стига.
- 75 минава filter, map го превръща в текста „75 €“, toArray() го събира.
- 40 също отпада. Всеки елемент минава целия път, преди да тръгне следващият.
- 130 и 58 минават. Масивът prices не се променя.
- Резултатът е нов масив с три текста.
Петте цени тръгват една по една. 12 стига до filter и отпада. 75 минава, map го прави на текста „75 €“, и toArray() го събира. После 40 отпада, а 130 и 58 минават.
Всеки елемент изминава целия път, преди да тръгне следващият. И нищо не тръгва, докато toArray() не поиска стойностите.
По-точно, за любопитните
Това се казва мързеливо изпълнение. Методите на итератора само записват какво трябва да се направи. Виж го с отпечатване във всяка стъпка:
const check = (p) => { console.log("filter " + p); return p >= 50;};const double = (p) => { console.log("map " + p); return p * 2;};
console.log([12, 75, 40].filter(check).map(double));
const stream = [12, 75, 40].values().filter(check).map(double);console.log("Итераторът е готов, нищо не е минало през него.");console.log(stream.toArray());filter 12filter 75filter 40map 75[ 150 ]Итераторът е готов, нищо не е минало през него.filter 12filter 75map 75filter 40[ 150 ]- Масивът (първият
console.log) прави всичко веднага и стъпка по стъпка:filterминава трите цени, чак послеmapминава тази, която е останала. - Итераторът е готов, преди през него да е минал и един елемент. После
map 75е веднага следfilter 75: елементите не чакат всички да минат презfilter. - Затова от итератора може да се вземат само първите няколко стойности с
take, без да се проверяват останалите:
const check = (p) => { console.log("filter " + p); return p >= 50;};
console.log([75, 130, 58, 12, 99].values().filter(check).take(2).toArray());filter 75filter 130[ 75, 130 ]За обикновените задачи масивът е достатъчен и се чете по-лесно. Итераторът е за много данни или когато трябват само първите няколко.
Идеята
Сбор, брой, средно
Резултатът може да е число вместо списък. Сборът на четните числа:
const numbers = [1, 2, 3, 4, 5];const sum = numbers.filter((n) => n % 2 === 0).reduce((total, n) => total + n, 0);console.log(`Сума на четните числа: ${sum}`);Сума на четните числа: 6reduce(функция, начало) свежда масива до една стойност. Започва от 0, а функцията получава сбора досега и следващия елемент и връща новия сбор.Същото за поръчките:
const big = orders.filter((o) => o.total >= 50).length;const revenue = orders.reduce((sum, o) => sum + o.total, 0);const average = orders.length > 0 ? revenue / orders.length : 0;const maxTotal = Math.max(...orders.map((o) => o.total));
console.log(`Поръчки над 50 €: ${big}`);console.log(`Оборот: ${revenue.toFixed(2)} €`);console.log(`Средно: ${average.toFixed(2)} €`);console.log(`Най-голяма: ${maxTotal.toFixed(2)} €`);Поръчки над 50 €: 3Оборот: 312.40 €Средно: 62.48 €Най-голяма: 120.00 €lengthна филтрирания масив е броят.- Средното е сборът, делено на броя. Ако поръчки няма,
0 / 0еNaN(„не е число“), затова проверкатаorders.length > 0. Math.max(...масив)дава най-голямото число....разгъва масива на отделни параметри:Math.max(75.5, 12.9, ...). Ако масивът е празен, резултатът е-Infinity. Какво да правиш с резултат, който може да липсва, е следващата идея.
По-точно, за любопитните
С празен масив:
const prices = [];console.log(prices.reduce((sum, p) => sum + p, 0));console.log(Math.max(...prices));console.log(0 / prices.length);0-InfinityNaNБез начална стойност reduce на празен масив спира с TypeError: Reduce of empty array with no initial value. Затова началната стойност се пише винаги.
Идеята
Подреди, отрежи, без повторения
const ratings = [5, 3, 5, 4, 2, 4, 5];console.log([...new Set(ratings)].toSorted((a, b) => a - b));
const top = orders .toSorted((a, b) => b.total - a.total) .slice(0, 3) .map((o) => o.customer);console.log(top);
Array.from({ length: 3 }, (_, i) => i + 1).forEach((i) => console.log(`Маса ${i}`));[ 2, 3, 4, 5 ][ 'Димитър', 'Мария', 'Ана' ]Маса 1Маса 2Маса 3new Set(ratings)маха повторенията: в множеството всяка стойност е веднъж.[...множество]го превръща обратно в масив.toSorted((a, b) => b.total - a.total)подрежда от най-голямата поръчка, с функция за сравнение от предишния урок.slice(0, 3)взима елементите от номер 0 до номер 3, без него: първите три. Заедно с подреждането това е „топ 3“.rangeв JavaScript няма.Array.from({ length: 3 }, (_, i) => i + 1)прави масив с 3 елемента, като за всеки номерiвика функцията: 1, 2, 3._е името на параметър, който не ни трябва.
По-точно, за любопитните
Set пази стойностите в реда на добавяне, затова без подреждане остава редът на първата поява:
const ratings = [5, 3, 5, 4, 2, 4, 5];console.log([...new Set(ratings)]);[ 5, 3, 4, 2 ]Пробвай
Кое стига до края?
Какво ще отпечата програмата?
const drinks = ["кафе", "чай", "кафе", "вода", "чай", "сок"];const result = [...new Set(drinks)] .toSorted() .slice(0, 3) .map((d) => d.toUpperCase());console.log(result);new Set оставя кафе, чай, вода, сок. toSorted() ги подрежда по азбука: вода, кафе, сок, чай. slice(0, 3) оставя първите три, а map ги прави с главни букви.Идеята
undefined: резултат, който може да липсва
Коя е най-голямата поръчка? Ако масивът е празен, отговор няма. Тогава резултатът е undefined: „няма стойност“. Но викащият трябва да помни да провери. Забрави ли, програмата спира с TypeError някъде по-късно.
Java за това има Optional: кутия, в която има стойност или е празна. В JavaScript такава кутия няма, но има два оператора, с които проверката е кратка:
const biggest = orders.toSorted((a, b) => b.total - a.total)[0];if (biggest !== undefined) { console.log(`Най-голяма: ${biggest.customer}`);}
const huge = orders.find((o) => o.total > 500);console.log(huge !== undefined);console.log(huge?.customer ?? "няма такава");Най-голяма: Димитърfalseняма такава[0]на празен масив еundefined.find(условие)дава първия елемент, за който условието е вярно, илиundefined.huge?.customer: акоhugeеundefinedилиnull, целият израз еundefined, вместо програмата да спре. Това се казва незадължителна верига (optional chaining).x ?? yдаваy, акоxеundefinedилиnull, иначеx.
Твоите функции обикновено връщат null, когато нямат отговор: „нарочно няма“. ?? работи и с него:
function nickname(user) { // Връща прякора или null, ако човекът няма. return user === "maria" ? "Мими" : null;}const maria = nickname("maria");if (maria !== null) { console.log(`Прякор: ${maria}`);}
const georgi = nickname("georgi");console.log(georgi ?? "без прякор");Прякор: Мимибез прякорПо-точно, за любопитните
- Без
?.:
const huge = orders.find((o) => o.total > 500);console.log(huge.customer);TypeError: Cannot read properties of undefined (reading 'customer')x || yсъщо дава резерва, но смята за липса и празния текст, и0.??не ги смята (третият ред е празен текст):
console.log(null ?? "без прякор");console.log("" || "без прякор");console.log("" ?? "без прякор");console.log(0 ?? 5);без прякорбез прякор
0someиeveryотговарят сtrueилиfalseдали някой или всички елементи отговарят на условието:
console.log(orders.some((o) => o.total > 100));console.log(orders.every((o) => o.total >= 10));truetrueПробвай
Намери грешката
Избери реда с грешката.
sort не прави нов масив. Той подрежда самия prices и връща него, затова sorted и prices са един и същ масив:
Най-скъпа: 130Първата по ред: 130Поправката е toSorted на ред 2. Той връща нов подреден масив, а prices остава 12, 75, 40, 130.
Итераторът от values() пък минава само веднъж, като потока в Java. Вторият toArray() дава празен масив:
const prices = [12, 75, 40, 130];const expensive = prices.values().filter((p) => p >= 50);console.log(expensive.toArray());console.log(expensive.toArray());[ 75, 130 ][]Идеята
Групиране и съединяване
Колко поръчки има от всеки град и какъв е оборотът?
const byCity = Map.groupBy(orders, (o) => o.city);const cities = [...byCity.keys()].toSorted();
const countByCity = Object.fromEntries(cities.map((city) => [city, byCity.get(city).length]));console.log(countByCity);
for (const city of cities) { const sum = byCity.get(city).reduce((total, o) => total + o.total, 0); console.log(`${city}: ${sum.toFixed(2)} €`);}
const customers = orders .map((o) => o.customer) .toSorted() .join(", ");console.log(customers);{ 'Варна': 2, 'Пловдив': 1, 'София': 2 }Варна: 76.90 €Пловдив: 120.00 €София: 115.50 €Ана, Георги, Димитър, Елена, МарияMap.groupBy(масив, функция)даваMap: за всеки град масив с поръчките му.[...byCity.keys()]прави масив от ключовете, аtoSorted()ги подрежда по азбука.Object.fromEntries(двойки)прави обект от двойки[ключ, стойност]. Тук ключът е градът, а стойността е броят на поръчките му (length).join(", ")съединява текстовете в един, със запетая между тях.
Map пази ключовете в реда на добавяне: София, Варна, Пловдив. Затова градовете се подреждат изрично.
По-точно, за любопитните
- За всеки град масив с имената. Тук градовете са в реда на добавяне:
const namesByCity = new Map();for (const [city, list] of Map.groupBy(orders, (o) => o.city)) { namesByCity.set(city, list.map((o) => o.customer));}console.log(namesByCity);Map(3) { 'София' => [ 'Мария', 'Елена' ], 'Варна' => [ 'Георги', 'Ана' ], 'Пловдив' => [ 'Димитър' ]}- Ако в двойките за
Object.fromEntriesключ се повтори, последната стойност печели, без грешка (в JavatoMapхвърля изключение):
const lastByCity = Object.fromEntries(orders.map((o) => [o.city, o.customer]));console.log(lastByCity);{ 'София': 'Елена', 'Варна': 'Ана', 'Пловдив': 'Димитър' }Упражнение
Отчети с методите на масива
ask, както досега.1. Продажби по категория. Клас Sale с продукт, категория, брой и единична цена и метод revenue() (брой по цена). Групирай по категория с Map.groupBy, сумирай всяка група с reduce, отпечатай категориите по азбука и общия оборот. Продажбите: кафе на зърна, напитки, 12 бр. по 18,50 €; чай, напитки, 30 по 4,20 €; шоколад, сладки, 40 по 2,80 €; бисквити, сладки, 25 по 3,10 €; мед, храни, 8 по 12 €; пак кафе на зърна, 5 по 18,50 €.
Напитки: 440.50 €Сладки: 189.50 €Храни: 96.00 €Общо: 726.00 €2. Топ 3 продукта. Със същите продажби намери трите продукта с най-голям оборот. Кафето е в две продажби, затова първо групирай по продукт и сумирай, после подреди записите на речника по стойност. Подсказка: [...byProduct].toSorted((a, b) => b[1] - a[1]).slice(0, 3): всеки елемент на Map е двойка [продукт, оборот].
Кафе на зърна: 314.50 €Чай: 126.00 €Шоколад: 112.00 €3. Средна оценка. Прочети един ред с оценки, разделени с интервал. Отпечатай средната, най-високата и колко са отличните (5,50 и нагоре). Подсказка: (await ask("")).split(" ").map(Number).
4.50 5.75 6.00 3.00 5.50Средно: 4.95Най-висока: 6.00Отлични: 34. Думите в текст. Прочети ред текст и отпечатай думите в него без повторения, по азбучен ред, с малки букви, разделени със запетая. Раздели по всичко, което не е буква: split(/[^\p{L}]+/u), и махни празните низове.
Кафе с мляко, кафе без захар. Чай с мед!без, захар, кафе, мед, мляко, с, чайОще задачи
- Най-дългата дума. С
reduceнамери най-дългата дума в изречение. Какво ще стане за празен ред без начална стойност? - Средно по клас. Ученици с име, клас и оценка: групирай ги с
Map.groupByи отпечатай средната оценка за всеки клас. - Големи и малки поръчки. С
someпровери дали има поръчка над 1000 €, а сeveryдали всички са поне 10 €.
Тест