25. ledna 2025

Mapa a množina

Prozatím jsme se naučili dvěma následujícím složitým datovým strukturám:

  • Objekty se používají k ukládání kolekcí hodnot pod klíči.
  • Pole se používají k ukládání seřazených kolekcí.

Pro skutečný život to však nestačí. Proto existují také Map (mapa) a Set (množina).

Mapa

Map je kolekce datových prvků uložených pod klíči, podobně jako Object. Hlavní rozdíl je však v tom, že Map umožňuje klíče libovolného typu.

Její metody a vlastnosti jsou:

  • new Map() – vytvoří mapu.
  • map.set(klíč, hodnota) – uloží hodnotu hodnota pod klíčem klíč.
  • map.get(klíč) – vrátí hodnotu uloženou pod klíčem klíč, jestliže klíč v mapě neexistuje, vrátí undefined.
  • map.has(klíč) – vrátí true, jestliže klíč v mapě existuje, jinak false.
  • map.delete(klíč) – odstraní prvek (dvojici klíč/hodnota) uložený pod klíčem klíč.
  • map.clear() – odstraní z mapy všechny prvky.
  • map.size – vrátí aktuální počet prvků.

Příklad:

let mapa = new Map();

mapa.set('1', 'řetězec1'); // řetězcový klíč
mapa.set(1, 'číslo1');     // číselný klíč
mapa.set(true, 'bool1');   // booleovský klíč

// pamatujete si na obvyklý Object? ten převádí klíče na řetězce
// Map si pamatuje typ klíče, takže tyto dva klíče jsou rozdílné:
alert( mapa.get(1)   ); // 'číslo1'
alert( mapa.get('1') ); // 'řetězec1'

alert( mapa.size ); // 3

Jak vidíme, na rozdíl od objektů zde nejsou klíče převáděny na řetězce. Jsou povoleny klíče jakýchkoli typů.

mapa[klíč] není správný způsob, jak používat mapu

Ačkoli mapa[klíč] funguje také, např. můžeme nastavit mapa[klíč] = 2, v tomto případě se s mapou zachází jako s planým JavaScriptovým objektem, takže zde platí všechna příslušná omezení (jen řetězcové/symbolické klíče a podobně).

Měli bychom tedy používat metody mapy: set, get a tak dále.

Mapa může používat jako klíče i objekty.

Příklad:

let jan = { jméno: "Jan" };

// pro každého uživatele budeme ukládat počet jeho návštěv
let mapaPočetNávštěv = new Map();

// jan je klíč mapy
mapaPočetNávštěv.set(jan, 123);

alert( mapaPočetNávštěv.get(jan) ); // 123

Používání objektů jako klíčů je jedna z nejpozoruhodnějších a nejdůležitějších vlastností map. Pro Object to neplatí. Řetězec jako klíč objektu je správně, ale jako klíč objektu nemůžeme použít jiný Object.

Zkusme to:

let jan = { jméno: "Jan" };
let ben = { jméno: "Ben" };

let objPočetNávštěv = {}; // zkusíme použít objekt

objPočetNávštěv[ben] = 234; // zkusíme použít jako klíč objekt ben
objPočetNávštěv[jan] = 123; // zkusíme použít jako klíč objekt jan, objekt ben bude nahrazen

// Toto bude zapsáno!
alert( objPočetNávštěv["[object Object]"] ); // 123

Jelikož objPočetNávštěv je objekt, převede všechny klíče typu Object, např. uvedené jan a ben, na stejný řetězec "[object Object]". To rozhodně není to, co jsme chtěli.

Jak Map porovnává klíče

Pro testování ekvivalence klíčů Map používá algoritmus SameValueZero. Je téměř stejný jako striktní rovnost ===, ale rozdíl spočívá v tom, že NaN se považuje za rovné NaN. Jako klíč tedy můžeme použít i NaN.

Tento algoritmus nemůžeme změnit nebo si ho přizpůsobit.

Zřetězení

Každé volání mapa.set vrátí samotnou mapu, takže volání můžeme „zřetězit“:

mapa.set('1', 'řetězec1')
  .set(1, 'číslo1')
  .set(true, 'bool1');

Iterace nad mapou

Pro procházení prvků mapy existují 3 metody:

  • mapa.keys() – vrátí iterovatelný objekt klíčů,
  • mapa.values() – vrátí iterovatelný objekt hodnot,
  • mapa.entries() – vrátí iterovatelný objekt dvojic [klíč, hodnota], používá se standardně ve for..of.

Příklad:

let mapaRecept = new Map([
  ['okurky',  500],
  ['rajčata', 350],
  ['cibule',  50]
]);

// iterace nad klíči (zelenina)
for (let zelenina of mapaRecept.keys()) {
  alert(zelenina); // okurky, rajčata, cibule
}

// iterace nad hodnotami (množství)
for (let množství of mapaRecept.values()) {
  alert(množství); // 500, 350, 50
}

// iterace nad dvojicemi [klíč, hodnota]
for (let dvojice of mapaRecept) { // totéž jako mapaRecept.entries()
  alert(dvojice); // okurky,500 (a tak dále)
}
Zachovává se pořadí vložení

Iterace probíhá ve stejném pořadí, v jakém byly hodnoty vloženy. Map toto pořadí na rozdíl od Object zachovává.

Navíc Map obsahuje vestavěnou metodu forEach, podobně jako Array:

// spustí tuto funkci pro každou dvojici (klíč, hodnota)
mapaRecepty.forEach( (hodnota, klíč, mapa) => {
  alert(`${klíč}: ${hodnota}`); // okurky: 500 atd.
});

Object.entries: mapa z objektu

Když je vytvořena mapa, můžeme do ní pro inicializaci předat pole (nebo jiný iterovatelný objekt) dvojic klíč/hodnota, například:

// pole dvojic [klíč, hodnota]
let mapa = new Map([
  ['1',  'řetězec1'],
  [1,    'číslo1'],
  [true, 'bool1']
]);

alert( mapa.get('1') ); // řetězec1

Máme-li planý objekt a rádi bychom z něj vytvořili mapu, můžeme použít vestavěnou metodu Object.entries(obj), která vrací pole dvojic klíč/hodnota objektu přesně v tomto formátu.

Můžeme tedy vytvořit mapu z objektu takto:

let obj = {
  jméno: "Jan",
  věk: 30
};

let mapa = new Map(Object.entries(obj));

alert( mapa.get('jméno') ); // Jan

Zde Object.entries vrací pole dvojic klíč/hodnota: [ ["jméno","Jan"], ["věk", 30] ]. To je přesně to, co potřebuje Map.

Object.fromEntries: objekt z mapy

Právě jsme viděli, jak vytvořit Map z planého objektu pomocí Object.entries(obj).

Existuje i metoda Object.fromEntries, která provádí opak – když jí předáme pole dvojic [klíč, hodnota], vytvoří z něj objekt:

let ceny = Object.fromEntries([
  ['banán', 1],
  ['pomeranč', 2],
  ['maso', 4]
]);

// nyní ceny = { banán: 1, pomeranč: 2, maso: 4 }

alert(ceny.pomeranč); // 2

Použitím Object.fromEntries můžeme získat z mapy planý objekt.

Například uložíme do mapy data, ale potřebujeme je předat kódu třetí strany, který očekává planý objekt.

Postupujeme takto:

let mapa = new Map();
mapa.set('banán', 1);
mapa.set('pomeranč', 2);
mapa.set('maso', 4);

let obj = Object.fromEntries(mapa.entries()); // vytvoří planý objekt (*)

// hotovo!
// obj = { banán: 1, pomeranč: 2, maso: 4 }

alert(obj.pomeranč); // 2

Volání mapa.entries() vrací iterovatelný objekt dvojic klíč/hodnota, přesně ve správném formátu pro Object.fromEntries.

Řádek (*) můžeme také zkrátit:

let obj = Object.fromEntries(mapa); // vypustíme .entries()

To je totéž, protože Object.fromEntries očekává jako argument iterovatelný objekt, ne nutně pole. A standardní iterace mapy vrací stejné dvojice klíč/hodnota jako mapa.entries(). Dostaneme tedy planý objekt se stejnými dvojicemi klíč/hodnota, jaké obsahuje mapa.

Množina

Množina Set je speciální typ kolekce – „množina hodnot“ (bez klíčů), v níž se každá hodnota může vyskytnout pouze jednou.

Její hlavní metody jsou:

Hlavní vlastností množiny je, že opakovaná volání množina.add(hodnota) se stejnou hodnotou nic neudělají. To je důvod, proč se každá hodnota v množině objeví pouze jednou.

Například máme přicházející návštěvníky a rádi bychom si je všechny pamatovali. Avšak opakované návštěvy by neměly vést ke zdvojení. Každý návštěvník musí být „započítán“ jen jednou.

Set je pro tento účel to pravé:

let množina = new Set();

let jan = { jméno: "Jan" };
let petr = { jméno: "Petr" };
let marie = { jméno: "Marie" };

// návštěvy, někteří uživatelé přišli vícekrát
množina.add(jan);
množina.add(petr);
množina.add(marie);
množina.add(jan);
množina.add(marie);

// množina si pamatuje jen unikátní hodnoty
alert( množina.size ); // 3

for (let uživatel of množina) {
  alert(uživatel.jméno); // Jan (pak Petr a Marie)
}

Alternativou pro Set by mohlo být pole uživatelů a kód, který při každém vložení hledá duplikáty pomocí pole.find. Výkon by však byl mnohem horší, protože tato metoda prochází celým polem a ověřuje každý prvek. Set je interně pro kontrolu unikátnosti mnohem lépe optimalizována.

Iterace nad množinou

Množinu můžeme procházet buď pomocí for..of, nebo pomocí forEach:

let množina = new Set(["pomeranče", "jablka", "banány"]);

for (let hodnota of množina) alert(hodnota);

// totéž s forEach:
množina.forEach((hodnota, hodnotaZnovu, množina) => {
  alert(hodnota);
});

Všimněte si něčeho veselého. Funkce předávaná do forEach má 3 argumenty: hodnota, pak stejnou hodnotu hodnotaZnovu a pak cílový objekt. Opravdu, stejná hodnota se v argumentech objevuje dvakrát.

To slouží ke kompatibilitě s Map, v níž funkce předávaná do forEach má tři argumenty. Jistě, vypadá to trochu zvláštně. Může to však pomoci v některých případech snadno nahradit mapu množinou a naopak.

Množina také poskytuje stejné metody, jaké má Map pro iterátory:

  • množina.keys() – vrátí iterovatelný objekt s hodnotami,
  • množina.values() – totéž jako množina.keys(), existuje kvůli kompatibilitě s Map,
  • množina.entries() – vrátí iterovatelný objekt s dvojicemi [hodnota, hodnota], existuje kvůli kompatibilitě s Map.

Shrnutí

Map – je kolekce hodnot s klíči.

Metody a vlastnosti:

  • new Map([iterovatelnýObjekt]) – vytvoří mapu, nepovinný objekt iterovatelnýObjekt (např. pole) obsahuje dvojice [klíč,hodnota] pro inicializaci.
  • mapa.set(klíč, hodnota) – uloží hodnotu hodnota pod klíčem klíč, vrátí samotnou mapu.
  • mapa.get(klíč) – vrátí hodnotu uloženou pod klíčem klíč, jestliže klíč v mapě neexistuje, vrátí undefined.
  • mapa.has(klíč) – vrátí true, jestliže klíč v mapě existuje, jinak false.
  • mapa.delete(klíč) – odstraní prvek uložený pod klíčem klíč, vrátí true, jestliže klíč v okamžiku volání v mapě existoval, jinak false.
  • mapa.clear() – odstraní z mapy všechny prvky.
  • mapa.size – vrátí aktuální počet prvků.

Rozdíly oproti běžnému objektu:

  • Klíče mohou být libovolného typu včetně objektů.
  • Obsahuje další užitečné metody, vlastnost size.

Set – je kolekce unikátních hodnot.

Metody a vlastnosti:

Iterace nad mapou a množinou probíhá vždy ve stejném pořadí, v jakém byly prvky vloženy. Nemůžeme tedy říci, že tyto kolekce nejsou seřazené, ale nemůžeme prvky seřadit jinak ani přímo získat prvek na určitém pořadí.

Úlohy

důležitost: 5

Nechť pole je nějaké pole.

Vytvořte funkci unikát(pole), která vrátí pole obsahující všechny různé prvky pole.

Příklad:

function unikát(pole) {
  /* váš kód */
}

let řetězce = ["Haré", "Kršna", "Haré", "Kršna",
  "Kršna", "Kršna", "Haré", "Haré", ":-O"
];

alert( unikát(řetězce) ); // Haré, Kršna, :-O

P.S. Zde jsou použity řetězce, ale mohou to být hodnoty libovolného typu.

P.P.S. K uložení unikátních hodnot použijte Set.

Otevřít pískoviště s testy.

function unikát(pole) {
  return Array.from(new Set(pole));
}

Otevřít řešení s testy na pískovišti.

důležitost: 4

Anagramy neboli přesmyčky jsou slova, která obsahují stejné počty stejných písmen, ale v jiném pořadí.

Příklad:

rak - kra
kostel - stolek
reklama - makrela - karamel

Napište funkci odstraňAnagramy(pole), která vrátí pole zbavené anagramů.

Příklad:

let pole = ["rak", "reklama", "makrela", "KRA", "kostel", "stolek", "karamel"];

alert( odstraňAnagramy(pole) ); // "rak,karamel,kostel" nebo "KRA,makrela,stolek"

Z každé skupiny přesmyček by mělo zbýt pouze jedno slovo. Nezáleží na tom, které.

Otevřít pískoviště s testy.

Pro nalezení anagramů rozdělíme každé slovo na písmena a ta seřadíme podle abecedy. Po seřazení písmen budou všechny anagramy stejné.

Příklad:

rak, kra -> akr
kostel, stolek -> eklost
reklama, makrela, karamel -> aaeklmr
...

Varianty slov se seřazenými písmeny použijeme jako klíče mapy, abychom uložili pro každý klíč jen jednu hodnotu:

function odstraňAnagramy(pole) {
  let mapa = new Map();

  for (let slovo of pole) {
    // rozdělíme slovo na písmena, seřadíme je a znovu spojíme
    let seřazené = slovo.toLowerCase().split('').sort().join(''); // (*)
    mapa.set(seřazené, slovo);
  }

  return Array.from(mapa.values());
}

let pole = ["rak", "reklama", "makrela", "KRA", "kostel", "stolek", "karamel"];

alert( odstraňAnagramy(pole) );

Seřazení písmen se děje ve zřetězeném volání na řádku (*).

Pro přehlednost jej rozdělme na několik řádků:

let seřazené = slovo // KRA
  .toLowerCase() // kra
  .split('') // ['k','r','a']
  .sort() // ['a','k','r']
  .join(''); // akr

Dvě různá slova 'KRA' a 'rak' budou seřazena stejně na 'akr'.

Další řádek vloží slovo do mapy:

mapa.set(seřazené, slovo);

Jestliže příště přijde slovo se stejným seřazením písmen, přepíše v mapě předchozí hodnotu se stejným klíčem. Vždy tedy budeme mít pro každou seřazenou skupinu písmen nejvýše jedno slovo.

Nakonec Array.from(mapa.values()) vezme iterovatelný objekt nad hodnotami mapy (klíče ve výsledku nepotřebujeme), vytvoří z těchto hodnot pole a vrátí je.

Zde bychom mohli místo Map použít i planý objekt, neboť klíče jsou řetězce.

Řešení by pak mohlo vypadat následovně:

function odstraňAnagramy(pole) {
  let obj = {};

  for (let i = 0; i < pole.length; i++) {
    let seřazené = pole[i].toLowerCase().split("").sort().join("");
    obj[seřazené] = pole[i];
  }

  return Object.values(obj);
}

let pole = ["rak", "reklama", "makrela", "KRA", "kostel", "stolek", "karamel"];

alert( odstraňAnagramy(pole) );

Otevřít řešení s testy na pískovišti.

důležitost: 5

Chtěli bychom uložit pole klíčů mapy mapa.keys() do proměnné a pak na ní volat metody specifické pro pole, např. .push.

Tohle však nefunguje:

let mapa = new Map();

mapa.set("jméno", "Jan");

let klíče = mapa.keys();

// Chyba: klíče.push není funkce
klíče.push("další");

Proč? Jak můžeme opravit kód, aby klíče.push fungovalo?

Je to proto, že mapa.keys() vrací iterovatelný objekt, ale ne pole.

Můžeme jej převést na pole pomocí Array.from:

let mapa = new Map();

mapa.set("jméno", "Jan");

let klíče = Array.from(mapa.keys());

klíče.push("další");

alert(klíče); // jméno,další
Mapa tutoriálu

Komentáře

přečtěte si před komentováním…
  • Máte-li návrhy na zlepšení, vytvořte prosím issue na GitHubu nebo pull request místo komentáře.
  • Pokud v článku něčemu nerozumíte, napište prosím, čemu přesně a na kterém místě.
  • Pro vložení několika slov kódu použijte značku <code>, pro několik řádků je obalte značkou <pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)