12k
All articles

Arrays in JavaScript mit Object.groupBy gruppieren

Object.groupBy in JavaScript gruppiert Arrays nach Schlüssel, vergleicht reduce und Map.groupBy und erklärt String-Coercion sowie null-prototypige Ergebnisse.

OpenReplay Team
OpenReplay Team
Arrays in JavaScript mit Object.groupBy gruppieren

Object.groupBy(items, callback) gruppiert ein Array in einem einzigen Aufruf: Der Callback wird einmal pro Element ausgeführt, der zurückgegebene Wert dient als Gruppenname, und das Ergebnis ist ein Objekt, das unter jedem Namen ein Array der passenden Elemente enthält.

In vielen Codebases steckt dafür noch ein selbst geschriebenes reduce – dieselben paar Zeilen Accumulator-Boilerplate, von Datei zu Datei kopiert – oder ein Lodash-Import, der nur für einen einzigen groupBy-Aufruf am Leben gehalten wird. Beides ist nicht falsch, und beides funktioniert weiterhin; mit der nativen Methode ist jedoch keines von beidem mehr nötig.

Dieser Artikel zeigt den nativen Aufruf an einem konkreten Problem (Bestellungen nach Status), stellt ihn dem reduce gegenüber, das er ersetzt, erklärt, wann stattdessen Map.groupBy das richtige Werkzeug ist, und arbeitet die beiden Verhaltensweisen durch, über die man in der Produktion stolpert: Keys, die sich stillschweigend in Strings verwandeln, und ein Ergebnisobjekt ohne hasOwnProperty.

Die wichtigsten Punkte

  • Object.groupBy ruft seinen Callback mit zwei Argumenten auf, (element, index), und verwendet den Rückgabewert als Gruppen-Key.
  • Einen reduce-Accumulator durch Object.groupBy zu ersetzen, verbessert die Lesbarkeit, nicht die Performance; es gibt keinen Grund zu erwarten, dass die Ausführung schneller ist.
  • Verwenden Sie Map.groupBy, wenn der Gruppierungs-Key kein String ist: ein Objekt, ein Date oder eine Zahl, die als Zahl erhalten bleiben soll.
  • Eine Gruppierung nach Boolean oder Zahl mit Object.groupBy erzeugt die String-Keys "true" und "40", weil jeder Key zu einem Property-Key umgewandelt wird.
  • Das von Object.groupBy zurückgegebene Objekt hat einen Null-Prototype, deshalb wirft result.hasOwnProperty(...) einen TypeError; verwenden Sie Object.hasOwn oder kopieren Sie das Ergebnis per Spread.

Bestellungen in drei Zeilen nach Status gruppieren

Aus einem Array von Bestellobjekten erzeugt Object.groupBy in einem einzigen Ausdruck ein nach Status geschlüsseltes Objekt – ohne Accumulator und ohne Existenzprüfung.

const orders = [
  { id: 1, status: "shipped",  total: 40 },
  { id: 2, status: "pending",  total: 15 },
  { id: 3, status: "shipped",  total: 60 },
  { id: 4, status: "refunded", total: 22 },
];

const byStatus = Object.groupBy(orders, (order) => order.status);

console.log(Object.keys(byStatus));   // ["shipped", "pending", "refunded"]
console.log(byStatus.shipped.length); // 2

Die Object.groupBy()-Referenz auf MDN beschreibt den Vertrag: Das erste Argument ist ein beliebiges Iterable, nicht nur ein Array, und das Ergebnis enthält eine Property pro eindeutigem Key. Die Gruppen erscheinen in der Reihenfolge, in der ihr erstes Mitglied angetroffen wurde. Die Elemente innerhalb jeder Gruppe sind die ursprünglichen Objekte, keine Kopien – eine Mutation von byStatus.shipped[0] verändert also auch orders[0].

Wie funktioniert der Object.groupBy-Callback?

Der Callback erhält zwei Argumente – das aktuelle Element und seinen Index – und was er zurückgibt, wird zum Gruppen-Key für dieses Element. Die ECMA-262-Definition von Object.groupBy legt genau diese zwei Argumente fest; es gibt kein drittes „gesamtes Array“-Argument wie bei map oder filter.

Da der Key berechnet wird, ist der Callback nicht darauf beschränkt, ein Feld auszulesen. Jeder Ausdruck, der einen String liefert, funktioniert – einschließlich eines Vergleichs oder eines aus dem Index abgeleiteten Buckets:

const bySize = Object.groupBy(orders, (order) => (order.total >= 50 ? "large" : "small"));
// { small: [order 1, order 2, order 4], large: [order 3] }

const byHalf = Object.groupBy(orders, (_, index) => (index < 2 ? "first" : "second"));
// { first: [order 1, order 2], second: [order 3, order 4] }

Jedes Element landet in genau einer Gruppe. Erzeugen zwei Elemente denselben Key, teilen sie sich ein Array in Einfügereihenfolge.

JavaScript Group By mit reduce vs. Object.groupBy

Ersetzt man einen reduce-Accumulator durch Object.groupBy, entfallen das Startobjekt, die Existenzprüfung pro Element, die Array-Allokation, der Push und das Return; übrig bleibt die einzelne Zeile, die entscheidet, zu welcher Gruppe ein Element gehört.

Hier die Variante, die in den meisten Codebases bereits existiert – so kompakt geschrieben, wie es die logische Nullish-Zuweisung erlaubt:

const byStatusReduce = orders.reduce((acc, order) => {
  acc[order.status] ??= [];
  acc[order.status].push(order);
  return acc;
}, {});

Und dasselbe Ergebnis mit der nativen Methode:

const byStatus = Object.groupBy(orders, (order) => order.status);

Beide erzeugen äquivalente Gruppierungen. Der Unterschied liegt darin, was die Leserin im Kopf behalten muss. In der reduce-Form verteilt sich die Gruppierungsabsicht über ein Accumulator-Startobjekt, eine bedingte Allokation, eine Mutation und einen Rückgabewert – und jeder dieser vier Bestandteile kann subtil falsch sein. In der nativen Form bleibt nur die Key-Funktion zu prüfen.

Object.groupBy ist eine Verbesserung der Lesbarkeit, nicht der Performance. Beide Ansätze iterieren die Eingabe einmal und allozieren ein Array pro Gruppe, und es gibt keinen Grund zu erwarten, dass der native Aufruf schneller ist als ein gut geschriebenes reduce. Entscheiden Sie sich dafür, weil es Boilerplate beseitigt, nicht wegen eines Benchmarks.

Wann sollten Sie stattdessen Map.groupBy verwenden?

Verwenden Sie Map.groupBy, wenn der Gruppierungs-Key kein String ist: ein Objekt, ein Date oder eine Zahl, die als Zahl erhalten bleiben soll. Map.groupBy() nimmt denselben Callback mit zwei Argumenten und unterscheidet sich nur im Rückgabetyp: eine Map, deren Keys exakt die Werte sind, die der Callback zurückgegeben hat.

const byTotal = Map.groupBy(orders, (order) => order.total);

console.log([...byTotal.keys()]);           // [40, 15, 60, 22]
console.log(typeof [...byTotal.keys()][0]); // "number"
console.log(byTotal.get(40).length);        // 1

Die Keys kommen als Zahlen zurück, in Einfügereihenfolge, und werden mit .get() gelesen. MDNs eigenes Beispiel auf der Map.groupBy-Seite gruppiert nach Objektidentität – genau der Fall, den ein Objektliteral überhaupt nicht abbilden kann: Zwei verschiedene Objekte mit identischem Inhalt bleiben als Map-Keys unterschiedlich.

Object.groupByMap.groupBy
Key-TypZu String oder Symbol umgewandeltBeliebiger Wert, unverändert
RückgabetypObjekt mit Null-PrototypeMap
Gruppe auslesenresult.shippedresult.get(key)
Iterationsreihenfolge der KeysEinfügereihenfolge, außer integer-artige Keys werden aufsteigend sortiertEinfügereihenfolge

Warum verwandelt Object.groupBy Zahlen-Keys in Strings?

Eine Gruppierung nach Boolean oder Zahl mit Object.groupBy erzeugt die String-Keys "true" und "40", nicht die ursprünglichen Werte true und 40; Map.groupBy erhält die Originalwerte als Map-Keys. Was auch immer der Callback zurückgibt, muss als Property-Key enden – alles, was nicht bereits ein String oder ein Symbol ist, wird also unterwegs in einen String konvertiert. Das ist normales Objektverhalten, überrascht aber trotzdem jeden, der ein Ergebnis mit Boolean-Keys erwartet.

const byPaid = Object.groupBy(orders, (order) => order.status === "shipped");

console.log(Object.keys(byPaid));             // ["true", "false"]
console.log(typeof Object.keys(byPaid)[0]);   // "string"
console.log(byPaid[true] === byPaid["true"]); // true (Lookup wandelt ebenfalls um)

const byTotalObj = Object.groupBy(orders, (order) => order.total);
console.log(Object.keys(byTotalObj));         // ["15", "22", "40", "60"]

Im numerischen Fall passieren zwei Dinge. Die Summen wurden zu Strings, und sie kommen aufsteigend sortiert statt in Einfügereihenfolge zurück, weil integer-artige Property-Keys in aufsteigender numerischer Reihenfolge vor allen anderen String-Keys aufgezählt werden. Code, der über das Ergebnis iteriert und dabei die ursprüngliche Reihenfolge erwartet, rendert die Gruppen in falscher Ordnung – ohne dass irgendwo ein Fehler geworfen wird.

Session Replays von Gruppierungsfehlern zeigen häufig genau dieses Muster: eine Kategorie-Überschrift, in der true statt „Shipped“ steht, oder Buckets, die sortiert erscheinen, obwohl die Daten es nicht waren. Die Konsole ist sauber, die Datenstruktur sieht im Log korrekt aus, weil { true: [...] } identisch ausgegeben wird, egal ob der Key ein Boolean oder ein String ist – und erst wenn man die gerenderte UI neben dem Code-Pfad sieht, wird die Umwandlung offensichtlich.

Warum wirft hasOwnProperty bei einem Object.groupBy-Ergebnis einen Fehler?

Das von Object.groupBy zurückgegebene Objekt hat einen Null-Prototype, deshalb wirft result.hasOwnProperty("shipped") einen TypeError; verwenden Sie Object.hasOwn(result, "shipped") oder kopieren Sie das Ergebnis mit Spread-Syntax, wenn nachgelagerter Code ein gewöhnliches Objekt erwartet. MDN dokumentiert den Rückgabewert als Objekt mit Null-Prototype, was bedeutet: Nichts aus Object.prototype ist darüber erreichbar – kein hasOwnProperty, kein toString, kein valueOf.

const byStatus = Object.groupBy(orders, (o) => o.status);

byStatus.hasOwnProperty("shipped");
// TypeError: byStatus.hasOwnProperty is not a function

Object.hasOwn(byStatus, "shipped"); // true
"shipped" in byStatus;              // true
Object.keys(byStatus);              // ["shipped", "pending", "refunded"]
JSON.stringify(byStatus);           // funktioniert normal

const plain = { ...byStatus };      // gewöhnliches Objekt mit Object.prototype
plain.hasOwnProperty("shipped");    // true

MDN verweist auf Object.hasOwn als moderne Entsprechung zu hasOwnProperty; es ist seit März 2022 Baseline „Widely available“, Sie können also direkt darauf zugreifen. Object.keys, Object.entries, der in-Operator, JSON.stringify und Spread funktionieren alle mit dem Null-Prototype-Ergebnis, weil keines davon von der Prototype-Kette abhängt. Der Fehler tritt nur auf, wenn eine Hilfsfunktion – oft tief in einer Utility-Library oder einer Template-Engine – eine Methode auf dem Objekt selbst aufruft. Eine Gruppe, die stillschweigend nie gerendert wird, oder ein TypeError aus dem Inneren einer Render-Schleife sind die typischen Symptome.

Welche Browser unterstützen Object.groupBy und Map.groupBy?

Object.groupBy und Map.groupBy teilen denselben Support-Stand: Beide sind auf MDN als Baseline „Widely available“ markiert und seit März 2024 browserübergreifend verfügbar – für aktuelle Browser-Ziele braucht also keines von beiden einen Polyfill. Die Entscheidung zwischen ihnen hängt am Key: Ist der Gruppenname natürlicherweise ein String (ein Status, eine Kategorie, ein Teamname), liefert Object.groupBy ein schlicht aussehendes Objekt, das Sie mit Punktnotation indexieren können. Ist der Key ein Objekt, ein Date, eine Zahl, mit der Sie rechnen wollen, oder ein Boolean, das Sie als Boolean vergleichen möchten, hält Map.groupBy ihn intakt und vermeidet beide oben genannten Fallen.

Den Accumulator ersetzen

Ein reduce mit einer ??= []-Zeile kann immer dann zu einem einzeiligen Object.groupBy-Aufruf werden, wenn der Gruppierungs-Key ein String ist und nachgelagert niemand hasOwnProperty auf dem Ergebnis aufruft. Ist der Key etwas anderes, greifen Sie zu Map.groupBy und lesen die Gruppen mit .get() zurück. In beiden Fällen bleibt nur noch die Logik zu testen, die über die Gruppenzugehörigkeit entscheidet.

FAQs

Funktioniert Object.groupBy in TypeScript, und welchen Typ gibt es zurück?

Ja. TypeScript 5.4 hat Typdeklarationen für Object.groupBy und Map.groupBy ergänzt; sie sind verfügbar, wenn target oder lib in der tsconfig es2024 oder esnext einschließt – bei älteren lib-Einstellungen meldet TypeScript, dass groupBy auf ObjectConstructor nicht existiert. Object.groupBy ist als Partial Record typisiert, jede Gruppe ist also möglicherweise undefined und muss vor dem Indexzugriff geprüft werden. Map.groupBy ist als Map vom Key-Typ auf ein Array von Elementen typisiert.

Was passiert, wenn der Object.groupBy-Callback undefined oder null zurückgibt?

Das Element landet in einer Gruppe mit dem String-Key 'undefined' oder 'null', weil Object.groupBy jedes Callback-Ergebnis in einen Property-Key umwandelt. Es wird nichts übersprungen und kein Fehler geworfen, ein fehlendes Feld erzeugt also stillschweigend eine zusätzliche Gruppe. Map.groupBy behält den tatsächlichen Wert undefined oder null als Map-Key. Um solche Elemente auszuschließen, filtern Sie das Array vorher oder geben Sie einen Fallback-Key wie 'unknown' zurück.

Was ist der Unterschied zwischen Object.groupBy und Lodash groupBy?

Lodash groupBy gibt ein gewöhnliches Objekt zurück, das von Object.prototype erbt, hasOwnProperty funktioniert darauf also; Object.groupBy gibt ein Objekt mit Null-Prototype zurück. Lodash akzeptiert eine Kurzform mit Property-Namen wie groupBy(orders, 'status') und ruft einen Funktions-Iteratee mit einem Argument auf, dem Wert, während Object.groupBy eine Funktion verlangt und das Element sowie seinen Index übergibt. Lodash akzeptiert außerdem einfache Objekte als Eingabe; Object.groupBy akzeptiert jedes Iterable. Beide wandeln Keys in Strings um.

Wie gruppiere ich mit Object.groupBy nach mehreren Feldern?

Geben Sie aus dem Callback einen zusammengesetzten String zurück, indem Sie zum Beispiel Status und einen Größen-Bucket mit einem Trennzeichen verbinden, sodass Keys wie 'shipped:large' entstehen. Object.groupBy hat keinen Multi-Key-Modus; jedes Element erhält genau einen Property-Key. Brauchen Sie die Felder getrennt, verschachteln Sie die Aufrufe: zuerst nach Status gruppieren, dann Object.groupBy auf dem Array jeder Gruppe für das zweite Feld ausführen – das ergibt eine zweistufige Struktur, die als result.shipped.large gelesen wird.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.