Lekce 0.5 · JavaScript obecně
Pole II: reduce, řazení a seskupování
reduce do hloubky, toSorted, Object.groupBy, Set, flatMap a metody, které mění pole.
Načítám lekci…
Lekce 0.5 · JavaScript obecně
reduce do hloubky, toSorted, Object.groupBy, Set, flatMap a metody, které mění pole.
Načítám lekci…
V minulé lekci z pole vždycky vzniklo zase pole (map, filter), jedna položka (find) nebo ano/ne (some). Často ale potřebuješ z celého pole vyrobit jednu souhrnnou hodnotu: celkovou cenu košíku, nejdražší položku, počty podle kategorií.
Představ si pokladní, která markuje nákup. Začne s nulou na displeji a s každou položkou na pásu přičte její cenu. Na konci ukáže jedno číslo. Přesně tohle dělá reduce – a protože „mezisoučet“ může být cokoli (číslo, objekt, pole), umí toho překvapivě hodně.
Druhá polovina lekce patří řazení, seskupování a metodám, které tiše mění původní pole – a jejich bezpečným náhradám.
const prices = [12, 15, 6]const total = prices.reduce((sum, price) => sum + price, 0)// ↑ ↑ ↑// akumulátor položka počáteční hodnota// total = 33
reduce dostane dvě věci: funkci a počáteční hodnotu akumulátoru. Funkce se zavolá pro každou položku. Dostane „dosavadní výsledek“ (akumulátor) a aktuální položku a vrátí nový dosavadní výsledek. Ten dostane při dalším zavolání.
| Krok | sum (akumulátor) | price | funkce vrátí |
|---|---|---|---|
| 1. | 0 (počáteční hodnota) | 12 | 0 + 12 = 12 |
| 2. | 12 | 15 | 12 + 15 = 27 |
| 3. | 27 | 6 | 27 + 6 = 33 |
| konec | 33 – výsledek reduce |
const cart = [{ name: 'Jablko', price: 12, qty: 3 },{ name: 'Chléb', price: 45, qty: 1 },{ name: 'Mléko', price: 22, qty: 2 },]const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0) // 125const count = cart.reduce((sum, item) => sum + item.qty, 0) // 6const average = total / count // 20.83…// Maximum: akumulátor si pamatuje dosud největšíconst maxPrice = cart.reduce((max, item) => (item.price > max ? item.price : max), 0) // 45// Nejdražší POLOŽKA (objekt): akumulátor je dosud nejdražší položkaconst priciest = cart.reduce((best, item) => (item.price > best.price ? item : best)) // Chléb// (bez počáteční hodnoty začne prvním prvkem – tady je to v pořádku, pokud pole není prázdné)
const votes = ['ano', 'ne', 'ano', 'zdržel se', 'ano']const counts = votes.reduce((acc, vote) => {acc[vote] = (acc[vote] ?? 0) + 1return acc // nezapomeň vrátit akumulátor!}, {})// { ano: 3, ne: 1, 'zdržel se': 1 }// Totéž bez měnění akumulátoru (spread) – delší, ale „čisté“const counts = votes.reduce((acc, vote) => ({ ...acc, [vote]: (acc[vote] ?? 0) + 1 }), {})
const users = [{ id: 7, name: 'Jana' }, { id: 9, name: 'Petr' }]const byId = users.reduce((acc, user) => {acc[user.id] = userreturn acc}, {})// { 7: { id: 7, name: 'Jana' }, 9: { id: 9, name: 'Petr' } }byId[9].name // 'Petr' – rychlé hledání bez procházení pole// Kratší alternativa bez reduce:Object.fromEntries(users.map((u) => [u.id, u]))
Co vidíš: Čtyři položky a čtyři různé reducery. Tabulka ukazuje, co bylo v akumulátoru před každým krokem a co funkce vrátila.
Vyzkoušej:
Object.groupBy.Co z toho plyne: reduce je smyčka s pamětí. Funkce vždy dostane, co vrátila minule, a vrací novou verzi. Typ akumulátoru (číslo, objekt, pole) určuje počáteční hodnota.
Načítám interaktivní část…
Rozdělit pole do skupin podle nějaké vlastnosti umí moderní JavaScript jedním voláním:
const products = [{ name: 'Jablko', category: 'ovoce' },{ name: 'Mrkev', category: 'zelenina' },{ name: 'Hruška', category: 'ovoce' },]const groups = Object.groupBy(products, (p) => p.category)// {// ovoce: [{ name: 'Jablko', … }, { name: 'Hruška', … }],// zelenina: [{ name: 'Mrkev', … }],// }// Skupinu může určit i výpočetObject.groupBy(products, (p) => (p.name.length > 5 ? 'dlouhé' : 'krátké'))// Typické použití: projít skupinyfor (const [category, items] of Object.entries(groups)) {console.log(category, items.map((p) => p.name).join(', '))}// ovoce Jablko, Hruška// zelenina Mrkev
Řadí se metodou toSorted (nové pole) nebo starší sort (mění původní pole). Obě dostanou porovnávací funkci: ta dostane dvě položky a a b a vrátí číslo.
| Funkce vrátí | Znamená |
|---|---|
| záporné číslo | a patří PŘED b |
| kladné číslo | a patří ZA b |
| 0 | je to jedno, nech pořadí |
const prices = [40, 5, 100, 25]prices.toSorted((a, b) => a - b) // [5, 25, 40, 100] – vzestupněprices.toSorted((a, b) => b - a) // [100, 40, 25, 5] – sestupněprices // [40, 5, 100, 25] – toSorted původní pole nemění// Proč a - b? Když a < b, vyjde záporné → a jde dopředu. Přesně co chceme.
const names = ['Žofie', 'adam', 'Čeněk', 'Bára']names.toSorted() // ['Bára', 'adam', 'Čeněk', 'Žofie'] 😕 podle kódů znakůnames.toSorted((a, b) => a.localeCompare(b, 'cs')) // ['adam', 'Bára', 'Čeněk', 'Žofie'] ✅ česká abecedaconst users = [{ name: 'Petr', age: 25 },{ name: 'Jana', age: 31 },{ name: 'Eva', age: 25 },]users.toSorted((a, b) => a.age - b.age) // podle věkuusers.toSorted((a, b) => a.name.localeCompare(b.name, 'cs')) // podle jména// Podle dvou kritérií: nejdřív věk, při shodě jménousers.toSorted((a, b) => a.age - b.age || a.name.localeCompare(b.name, 'cs'))// Eva (25), Petr (25), Jana (31)// když a.age - b.age je 0 (falsy), použije se část za ||// Podle dataorders.toSorted((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) // nejnovější první// Booleany: nejdřív nehotovétasks.toSorted((a, b) => Number(a.done) - Number(b.done))
const letters = ['a', 'b', 'c', 'd', 'e']letters.slice(1, 3) // ['b', 'c'] – od indexu 1 do 3 (bez 3), nové poleletters.slice(2) // ['c', 'd', 'e'] – od 2 do konceletters.slice(-2) // ['d', 'e'] – poslední dvěletters.slice(0, 3) // ['a', 'b', 'c'] – první tři („top 3“)letters.slice() // kopie celého poleconst a = [1, 2]const b = [3, 4][...a, ...b] // [1, 2, 3, 4] – spojení[0, ...a, 2.5, ...b] // [0, 1, 2, 2.5, 3, 4] – s vložením hodnota.concat(b) // [1, 2, 3, 4] – totéž postaru['Jana', 'Petr'].join(', ') // 'Jana, Petr'['a', 'b'].join('') // 'ab'
[[1, 2], [3], [4, 5]].flat() // [1, 2, 3, 4, 5] – zploští o jednu úroveňconst posts = [{ title: 'A', tags: ['react', 'js'] },{ title: 'B', tags: ['css'] },]posts.map((p) => p.tags) // [['react', 'js'], ['css']] – pole políposts.flatMap((p) => p.tags) // ['react', 'js', 'css'] – map + flat v jednom
Set je kolekce, která si každou hodnotu pamatuje jen jednou. Ideální na odstranění duplicit:
const tags = ['react', 'js', 'react', 'css', 'js']const unique = new Set(tags) // Set { 'react', 'js', 'css' }unique.size // 3unique.has('css') // true[...unique] // ['react', 'js', 'css'] – zpátky na pole// Jednořádková klasikaconst uniqueTags = [...new Set(tags)]// Unikátní kategorie z pole objektůconst categories = [...new Set(products.map((p) => p.category))]
Pár starších metod nepíše nové pole, ale přepisuje to původní. V obyčejném skriptu to nevadí, ale když pole patří někomu jinému (třeba stavu v Reactu), je to zdroj nepříjemných chyb. Každá má bezpečnou náhradu:
| ❌ Mění původní pole | ✅ Vrací nové pole | Co dělá |
|---|---|---|
arr.push(x) | [...arr, x] | přidá na konec |
arr.unshift(x) | [x, ...arr] | přidá na začátek |
arr.pop() | arr.slice(0, -1) | odebere poslední |
arr.shift() | arr.slice(1) | odebere první |
arr.splice(i, 1) | arr.toSpliced(i, 1) | odebere na pozici i |
arr.splice(i, 0, x) | arr.toSpliced(i, 0, x) | vloží na pozici i |
arr[i] = x | arr.with(i, x) | nahradí na pozici i |
arr.sort(fn) | arr.toSorted(fn) | seřadí |
arr.reverse() | arr.toReversed() | obrátí pořadí |
const original = [3, 1, 2]const sorted = original.sort((a, b) => a - b)sorted // [1, 2, 3]original // [1, 2, 3] 😱 změnil se i originál – sorted a original jsou TOTÉŽ poleconst original2 = [3, 1, 2]const sorted2 = original2.toSorted((a, b) => a - b)sorted2 // [1, 2, 3]original2 // [3, 1, 2] ✅
Sečti čísla v poli a seřaď je od nejmenšího. Pole jsou zmrazená, takže sort neprojde.
VÝZVA: Součet a řazení 1. total([4, 1, 5]) má vrátit 10. Použij reduce s počáteční hodnotou 0. 2. sortAsc([4, 1, 5]) má vrátit [1, 4, 5]. Použij toSorted((a, b) => a - b).
Načítám interaktivní část…
První výzva kombinuje všechno z lekcí o polích na reálných datech produktů. Druhá procvičí reduce, Set, řazení a flatMap – a košík je zmrazený, takže sort neprojde.
Tabulka statistik je hotová, jen v ní jsou vymyšlená čísla. Nahraď je výpočty z pole produktů – každou hodnotu jedním výrazem.
VÝZVA: Statistiky obchodu (metody polí)
Data: pole `products` ze src/course/fakeApi.ts.
Každý produkt vypadá takto: { id: 1, name: 'Jablko', category: 'ovoce', price: 12, inStock: true }
Tabulka je hotová, jen jsou všechny hodnoty zatím „natvrdo“. Nahraď každou z nich
výpočtem z pole `products` – vždy jedním výrazem (klidně s řetězením metod).
1. `inStockCount` – kolik produktů je skladem (filter + length).
2. `stockValue` – součet cen produktů skladem (filter + reduce, počáteční hodnota 0).
3. `hasDairy` – je v nabídce aspoň jeden produkt z kategorie 'mléčné'? (some)
4. `allCheap` – stojí všechny produkty méně než 50 Kč? (every)
5. `product8` – název produktu s id 8 (find; find může vrátit undefined → použij ?.name)
6. `cheapest` – název nejlevnějšího produktu (toSorted podle ceny a vzít první položku).
7. `fruitNames` – názvy ovoce seřazené podle abecedy a spojené čárkou:
filter → map → toSorted((a, b) => a.localeCompare(b)) → join(', ')
Pole `products` přitom nesmíš změnit (žádné sort, push, splice…).Načítám interaktivní část…
Sedm funkcí, které z košíku spočítají souhrny: celkovou cenu, počty, ceny po kategoriích, unikátní hodnoty a žebříček nejdražších položek.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Object.groupBy rozdělí pole do skupin, Math.max(...arr) najde maximum čísel.(a, b) => a - b, texty a.localeCompare(b, 'cs'), víc kritérií přes ||. Bez funkce se řadí jako text.slice vyřízne, spread spojí, flatMap zploští, [...new Set(arr)] odstraní duplicity.toSpliced, with, toSorted, toReversed.Checks, deepFreeze, equal, Check)Checks, deepFreeze, equal, Check – ze souboru src/course/checks.tsx. Pomocník kurzu pro výzvy s funkcemi: spustí testy a ukáže ✅/❌. Jak funguje uvnitř, nemusíš řešit – stačí, že ti řekne, které kroky už máš hotové./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}/** Freezes an object and all nested objects and arrays – any attempt to change them throws. */export function deepFreeze<T>(value: T): T {if (value && typeof value === 'object') {Object.values(value).forEach(deepFreeze)Object.freeze(value)}return value}/** Compares two values by content (arrays and objects item by item), not by reference. */export function equal(a: unknown, b: unknown): boolean {return JSON.stringify(a) === JSON.stringify(b)}
products)products – ze souboru src/course/fakeApi.ts. Falešné API kurzu: data a funkce, které předstírají server (se zpožděním, občas i s chybou). Ve skutečné aplikaci by tu bylo volání fetch().export interface Product {id: numbername: stringcategory: 'ovoce' | 'zelenina' | 'pečivo' | 'mléčné'price: numberinStock: boolean}export const products: Product[] = [{ id: 1, name: 'Jablko', category: 'ovoce', price: 12, inStock: true },{ id: 2, name: 'Banán', category: 'ovoce', price: 8, inStock: true },{ id: 3, name: 'Hruška', category: 'ovoce', price: 15, inStock: false },{ id: 4, name: 'Mrkev', category: 'zelenina', price: 6, inStock: true },{ id: 5, name: 'Rajče', category: 'zelenina', price: 9, inStock: true },{ id: 6, name: 'Okurka', category: 'zelenina', price: 19, inStock: false },{ id: 7, name: 'Rohlík', category: 'pečivo', price: 3, inStock: true },{ id: 8, name: 'Chléb', category: 'pečivo', price: 45, inStock: true },{ id: 9, name: 'Mléko', category: 'mléčné', price: 22, inStock: true },{ id: 10, name: 'Sýr eidam', category: 'mléčné', price: 39, inStock: true },{ id: 11, name: 'Jogurt', category: 'mléčné', price: 14, inStock: false },{ id: 12, name: 'Pomeranč', category: 'ovoce', price: 11, inStock: true },]
row, stack, card, list, btn, input nebo muted jsou hotové styly kurzu v src/styles.css (row = prvky vedle sebe, stack = pod sebou, card = rámeček, muted = šedý text).