Az Object.groupBy() nem olyan objektumot ad vissza, amilyenre számítasz

Az Object.groupBy() null-prototípusú szótárat ad, a Map.groupBy() pedig az objektumkulcsok azonosságát is megőrzi. A két API ugyan csoportosít, de más adatmodellhez való.

Az Object.groupBy() kényelmesnek tűnik: beadunk egy tömböt és egy kulcsot előállító függvényt, visszakapjuk a csoportokat. A meglepetés akkor jön, amikor az eredményen megszokásból meghívjuk a hasOwnProperty-t. Nem létezik rajta. Ez nem hiányos implementáció, hanem szándékos API-döntés.

A Object.groupBy() null-prototípusú objektumot ad vissza. Vagyis az eredmény prototípusa null, nem Object.prototype. Nincs örökölt toString, constructor vagy hasOwnProperty; a csoportnevek viszont saját property-k. A specifikáció ezt közvetlenül null-prototípusú objektum létrehozásával írja elő.

A __proto__ is csak egy csoportnév

Ez a választás nem pusztán kényelmetlenség. Egy normál objektumnál a __proto__ név történelmileg különleges: a prototípushoz kötődik. A csoportosító API eredményében viszont biztonságosan lehet ilyen nevű saját kulcs. Külső adatból képzett címkéknél ez kifejezetten hasznos tulajdonság.

const rows = [
  { customer: 'acme', total: 120 },
  { customer: 'beta', total: 80 },
  { customer: '__proto__', total: 40 },
];

const byCustomer = Object.groupBy(rows, ({ customer }) => customer);

console.log(Object.getPrototypeOf(byCustomer) === null); // true
console.log(byCustomer.__proto__.map(({ total }) => total)); // [40]
console.log(Object.hasOwn(byCustomer, 'acme')); // true
console.log(byCustomer.hasOwnProperty); // undefined

A saját kulcs ellenőrzésére ezért az Object.hasOwn() a jó forma. Nem függ attól, van-e prototípus, és nincs szükség a régi, körülményes Object.prototype.hasOwnProperty.call(...) mintára sem. A példát Node 22.22.3-mal futtattam; a kimenet sorban true, [ 40 ], true, undefined.

A visszatérő értékből property-kulcs lesz

Az Object.groupBy() callbackjének eredménye property-kulccsá alakul. Egy 1 szám kulcsa tehát a "1" property, nem egy numerikus azonosító; a string és a szám ugyanabba a rekeszbe kerülhet. Ez teljesen természetes, ha a csoportok nevei például státuszok vagy dátumok, és jól együttműködik az objektumokkal, valamint a JSON-nal.

A csoportokban az eredeti elemek referenciái vannak, nem mély másolatok. Ha a byCustomer.acme[0].total mezőt módosítjuk, az eredeti rows[0] is módosul. A groupBy osztályoz, nem izolált adatszerkezetet épít.

Miért jobb ez a kézzel írt reduce()-nál?

A korábbi, elterjedt minta egy reduce() volt: (acc[key] ??= []).push(item). Ez rövid, de egy üres objektummal induló accumulatornál a __proto__ és más örökölt nevek külön kezelést kérnek. A körültekintő változat már Object.create(null)-t használ, amellyel ugyanezt a szótárviselkedést építjük fel kézzel. Az Object.create() pontosan ezt a prototípus nélküli alakot teszi lehetővé.

Az Object.groupBy() ezt a biztonságos alapot és a tömbök létrehozását adja készre. A reduce() továbbra is jobb választás, ha csoportosítás közben rögtön összeget, darabszámot vagy más aggregátumot akarunk számolni, mert ott nem kell köztes elem-listákat felépíteni. A két API nem teljesítményverseny: eltérő eredményformátumot céloznak.

Mikor kell inkább Map.groupBy()?

Ha a csoport kulcsa nem név, hanem maga egy objektum, a Map.groupBy() a pontosabb eszköz. A visszaadott Map nem alakítja property-névvé a callback értékét: objektumot, függvényt vagy más tetszőleges értéket kulcsként használhat. Így egy konfiguráció- vagy állapotobjektum azonossága valóban megmarad.

const open = { state: 'open' };
const closed = { state: 'closed' };

const byState = Map.groupBy(rows, row =>
  row.total >= 100 ? open : closed
);

console.log(byState.get(open)); // az acme sora
console.log([...byState.keys()].map(key => key.state));
// ['open', 'closed']

A Map.groupBy specifikációja ugyanúgy tömböket épít csoportonként, de a kulcsok a Map szabályai szerint maradnak meg. Cserébe a JSON.stringify() nem ad hasznos JSON-reprezentációt egy Map-ről; ha szerializálni kell, külön alakításra lesz szükség.

Két API, két adatmodell

  • Object.groupBy(): szöveges, külső adatból származó vagy JSON-ba kerülő csoportnevekhez. Az eredményt null-prototípusú szótárként kezeld, és Object.hasOwn()-t használj.
  • Map.groupBy(): amikor a kulcs típusa és azonossága számít, például objektumokhoz vagy függvényekhez kötött kategóriáknál.
  • Mindkettő: a callback futásán kívül nem módosítja az input tömböt, de a csoportok elemei ugyanazok az objektumok maradnak.

Az Object.groupBy() visszatérési értékét ezért nem érdemes „még egy sima objektumként” kezelni. Pont az a haszna, hogy szótárként viselkedik, és nem ütközik az Object.prototype örökségével. Ha ezt már a típus és az első property-ellenőrzés megválasztásánál figyelembe vesszük, az API sokkal kiszámíthatóbb lesz.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük