Arrays nativ gruppieren – ohne Lodash und ohne reduce()
Wer Arrays in JavaScript nach Kriterien gruppieren wollte, griff lange zu Lodash, Underscore oder einer verschachtelten reduce()-Kette. Mit Object.groupBy und Map.groupBy bringt ES2024 endlich eine native, lesbare Lösung direkt in die Sprache – mit einem klaren Unterschied zwischen beiden, der in der Praxis oft übersehen wird.
Inhaltsverzeichnis
- 1. Das Problem mit manueller Gruppierung
- 2. Syntax und Grundkonzept von Object.groupBy
- 3. Map.groupBy: Wenn der Schlüssel kein String sein darf
- 4. Object.groupBy vs. Map.groupBy: der entscheidende Unterschied
- 5. Praxisbeispiele: Bestellungen, Produkte, Nutzerdaten
- 6. Vergleich mit reduce() und Lodash groupBy
- 7. Null-Prototyp: Warum Object.groupBy kein normales Objekt zurückgibt
- 8. Iterable-Support: Nicht nur Arrays, sondern auch Sets und Maps
- 9. Browserkompatibilität und Polyfill-Strategie
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem mit manueller Gruppierung
Das Gruppieren von Array-Elementen nach einem Schlüssel ist eine der häufigsten Operationen in JavaScript-Anwendungen. Ob Bestellungen nach Status, Produkte nach Kategorie oder Log-Einträge nach Schweregrad – immer wieder landen Entwickler bei der gleichen Grundaufgabe. Lange Jahre war Array.prototype.reduce() die Standardantwort, und Bibliotheken wie Lodash lieferten eine komfortablere Variante unter dem Namen _.groupBy(). Beide Ansätze lösen das Problem, bringen aber unnötige Komplexität oder externe Abhängigkeiten mit sich.
Mit Object.groupBy und Map.groupBy hat das TC39-Komitee zwei statische Methoden in die JavaScript-Standardbibliothek aufgenommen, die genau diese Lücke schließen. Die Methoden wurden im Stage-3-Proposal „Array Grouping" entwickelt und sind seit Chrome 117, Firefox 119 und Safari 17.4 nativ verfügbar. Das Ergebnis ist Code, der ohne Lodash, ohne reduce-Boilerplate und ohne Typen-Cast auskommt – und trotzdem vollständig typsicher in TypeScript nutzbar ist.
2. Syntax und Grundkonzept von Object.groupBy
Object.groupBy ist eine statische Methode auf dem eingebauten Object-Objekt und nimmt zwei Argumente: ein iterierbares Objekt (z. B. ein Array) und eine Callback-Funktion, die für jedes Element den Gruppierungsschlüssel als String oder Symbol zurückgibt. Das Ergebnis ist ein Objekt, dessen Schlüssel die Rückgabewerte des Callbacks sind und dessen Werte Arrays der zugehörigen Elemente darstellen. Die originalen Elemente werden dabei nicht kopiert, sondern per Referenz übernommen – Mutationen am gruppierten Ergebnis wirken sich direkt auf die Quelldaten aus.
Der Callback erhält wie bei Array.prototype.map() das aktuelle Element, den Index und das ursprüngliche Array als Argumente. Gibt der Callback undefined zurück, wird das Element in der Gruppe mit dem Schlüssel "undefined" einsortiert – ein implizites Verhalten, das in der Praxis zu unerwarteten Ergebnissen führen kann. Wer sichergehen will, sollte den Callback defensiv gestalten und explizit einen Fallback-Schlüssel definieren. Object.groupBy verwendet intern String-Konvertierung für nicht-String-Schlüssel, weshalb Objekte als Schlüssel nicht sinnvoll funktionieren.
// Object.groupBy — basic usage with an order list
const orders = [
{ id: 1, status: "pending", total: 49.90 },
{ id: 2, status: "shipped", total: 129.00 },
{ id: 3, status: "pending", total: 79.50 },
{ id: 4, status: "delivered", total: 220.00 },
{ id: 5, status: "shipped", total: 59.90 },
];
// Group by status — callback must return a string or symbol
const byStatus = Object.groupBy(orders, order => order.status);
// Result shape:
// {
// pending: [{ id: 1, … }, { id: 3, … }],
// shipped: [{ id: 2, … }, { id: 5, … }],
// delivered: [{ id: 4, … }]
// }
console.log(byStatus.pending.length); // 2
console.log(byStatus.shipped[0].total); // 129
// Group by price range — computed key via callback
const byRange = Object.groupBy(orders, ({ total }) => {
if (total < 60) return "low";
if (total < 150) return "mid";
return "high";
});
console.log(byRange.low.map(o => o.id)); // [1, 5]
console.log(byRange.high.map(o => o.id)); // [4]
3. Map.groupBy: Wenn der Schlüssel kein String sein darf
Map.groupBy funktioniert identisch zu Object.groupBy, gibt aber statt eines einfachen Objekts eine Map-Instanz zurück. Der wesentliche Unterschied liegt in der Schlüsselbehandlung: Eine Map kann beliebige Werte als Schlüssel verwenden – Objekte, Arrays, Klassen-Instanzen oder primitive Werte. Das eröffnet Anwendungsfälle, die mit Object.groupBy nicht darstellbar sind, etwa das Gruppieren nach einem komplexen Kategorie-Objekt statt nach dessen String-Darstellung.
Die Rückgabe einer Map bedeutet auch, dass die Iterationsreihenfolge der Insertionsreihenfolge folgt – im Gegensatz zu normalen Objekten, bei denen numerische Schlüssel immer zuerst stehen. Das ist besonders relevant, wenn die Reihenfolge der Gruppen im UI oder in einer geordneten Ausgabe eine Rolle spielt. Mit map.get(key) statt obj[key] erfolgt der Zugriff, was sich in der Typisierung und bei der Verwendung in generischem Code anders anfühlt, aber mehr Kontrolle über Schlüsseltypen bietet.
4. Object.groupBy vs. Map.groupBy: der entscheidende Unterschied
Beide Methoden teilen dieselbe Signatur und dieselbe Semantik für die Callback-Funktion, unterscheiden sich aber fundamental im Rückgabetyp und damit in den erlaubten Schlüsseltypen. Object.groupBy konvertiert alle Schlüssel implizit zu Strings, weil JavaScript-Objekte nur String- und Symbol-Schlüssel kennen. Map.groupBy gibt eine echte Map zurück und behandelt Schlüssel mit strikter Gleichheit nach dem SameValueZero-Algorithmus – ähnlich wie ===, aber ohne die Sonderbehandlung von NaN.
Ein häufiger Fallstrick: Wer ein Objekt als Schlüssel in Object.groupBy zurückgibt, erhält alle Elemente in der Gruppe [object Object] – weil JavaScript das Objekt zu diesem String konvertiert. Mit Map.groupBy wird das Objekt selbst als Schlüssel verwendet, und jede separate Objekt-Instanz erzeugt eine eigene Gruppe. Das ist der Grund, warum bei Gruppen nach Domänenobjekten stets Map.groupBy gewählt werden sollte.
| Merkmal | Object.groupBy | Map.groupBy |
|---|---|---|
| Rückgabetyp | Null-Prototyp-Objekt | Map-Instanz |
| Erlaubte Schlüsseltypen | String, Symbol (andere werden zu String konvertiert) | Beliebige Werte inkl. Objekte und Arrays |
| Zugriff auf Gruppen | result.key oder result["key"] |
result.get(key) |
| Iterationsreihenfolge | Numerische Schlüssel zuerst, dann String-Reihenfolge | Streng nach Insertionsreihenfolge |
| Ideal für | String-Schlüssel, JSON-Ausgabe, einfache Lookups | Objekt-Schlüssel, geordnete Ausgabe, komplexe Domänenmodelle |
5. Praxisbeispiele: Bestellungen, Produkte, Nutzerdaten
In einer E-Commerce-Anwendung ist das Gruppieren von Produkten nach Kategorie oder Bestellungen nach Lieferstatus einer der häufigsten Anwendungsfälle für Object.groupBy. Statt wie früher eine reduce()-Funktion mit initialem leeren Objekt zu schreiben und dabei jeden Schlüssel defensiv zu initialisieren, genügt ein einziger Aufruf. Das Ergebnis ist direkt als Datenstruktur für UI-Komponenten nutzbar – etwa um Produkte je Kategorie in separate Listenabschnitte zu rendern oder Bestellungen nach Status in Kanban-Spalten aufzuteilen.
Für Nutzerdaten mit komplexen Profil-Objekten als Gruppierungsschlüssel – etwa "Alle Aktivitäten nach Nutzer-Objekt gruppieren" – ist Map.groupBy die richtige Wahl. Der Nutzer-Objekt-Schlüssel bleibt erhalten, und über map.forEach((activities, user) => …) kann sowohl auf die Gruppe als auch auf das vollständige Nutzer-Objekt zugegriffen werden, ohne es vorher in einen String-Schlüssel zu serialisieren und danach wieder zu deserialisieren.
// Practical example: group products by category object (Map.groupBy)
const electronics = { id: "cat-1", name: "Electronics" };
const clothing = { id: "cat-2", name: "Clothing" };
const products = [
{ name: "Laptop", price: 999, category: electronics },
{ name: "T-Shirt", price: 29, category: clothing },
{ name: "Monitor", price: 349, category: electronics },
{ name: "Jeans", price: 59, category: clothing },
];
// Map.groupBy preserves the object reference as the key
const byCategory = Map.groupBy(products, p => p.category);
byCategory.forEach((items, cat) => {
console.log(`${cat.name}: ${items.map(i => i.name).join(", ")}`);
// Electronics: Laptop, Monitor
// Clothing: T-Shirt, Jeans
});
// Object.groupBy with a derived string key — compute total per status
const orders = [
{ id: 1, status: "pending", total: 49.90 },
{ id: 2, status: "shipped", total: 129.00 },
{ id: 3, status: "pending", total: 79.50 },
];
const grouped = Object.groupBy(orders, o => o.status);
// Derive summary from grouped result
const summary = Object.entries(grouped).map(([status, items]) => ({
status,
count: items.length,
totalRevenue: items.reduce((sum, o) => sum + o.total, 0),
}));
console.log(summary);
// [{ status: "pending", count: 2, totalRevenue: 129.40 }, …]
6. Vergleich mit reduce() und Lodash groupBy
Die klassische Implementierung mit reduce() ist nicht falsch, aber sie erfordert drei Dinge, die mit Object.groupBy entfallen: das initiale leere Objekt als Akkumulator, die defensive Initialisierung des Array-Slots für jeden neuen Schlüssel und das explizite Zurückgeben des Akkumulators am Ende jedes Aufrufs. Bei verschachtelten Gruppierungen multipliziert sich dieser Boilerplate und macht den Code schwer lesbar. Object.groupBy eliminiert all das auf eine einzige Zeile.
Lodash _.groupBy() bietet dieselbe Lesbarkeit, bringt aber die gesamte Lodash-Bibliothek als Abhängigkeit mit – oder bei Tree-Shaking zumindest das Grouping-Modul. In modernen Projekten, die auf ES2024 abzielen, ist das nicht mehr notwendig. Wer Lodash nur wegen _.groupBy(), _.sortBy() und ähnlicher Array-Hilfsfunktionen eingebunden hat, kann die Abhängigkeit vollständig entfernen und native Methoden verwenden. Das spart Bundle-Größe, reduziert Sicherheitsupdates und macht den Code selbstdokumentierend, weil keine externe API zu kennen ist.
7. Null-Prototyp: Warum Object.groupBy kein normales Objekt zurückgibt
Object.groupBy gibt ein Objekt mit einem null-Prototyp zurück – das bedeutet, es hat keine geerbten Methoden wie toString(), hasOwnProperty() oder constructor. Das ist eine bewusste Designentscheidung: Weil das gruppierte Objekt potenziell Schlüssel wie constructor, toString oder __proto__ enthalten kann (wenn die Daten diese Werte tragen), würde ein normales Objekt mit Prototyp-Kette zu unerwarteten Kollisionen führen. Ein Null-Prototyp-Objekt verhindert das vollständig.
In der Praxis bedeutet das, dass man auf dem Ergebnis von Object.groupBy keine Methoden wie result.hasOwnProperty("pending") aufrufen kann – das würde einen TypeError werfen. Stattdessen verwendet man Object.hasOwn(result, "pending") oder prüft einfach direkt mit "pending" in result. Für die Serialisierung mit JSON.stringify() funktioniert das Null-Prototyp-Objekt wie ein normales Objekt – alle eigenen Eigenschaften werden berücksichtigt. Das Verhalten ist also für den typischen Anwendungsfall korrekt, muss aber bekannt sein, um Überraschungen zu vermeiden.
8. Iterable-Support: Nicht nur Arrays, sondern auch Sets und Maps
Sowohl Object.groupBy als auch Map.groupBy akzeptieren jedes Iterable als erstes Argument – nicht nur Arrays. Das schließt Set, Map, String, arguments-Objekte, Generator-Funktionen und alle Klassen ein, die das Iterator-Protokoll implementieren. Das ist besonders nützlich, wenn Daten bereits in einer anderen Datenstruktur vorliegen und eine Konvertierung zu einem Zwischenarray vermieden werden soll.
Ein Set von eindeutigen Tag-Strings lässt sich direkt mit Object.groupBy nach Anfangsbuchstabe oder Länge gruppieren, ohne erst Array.from(set) aufzurufen. Eine Map kann direkt über ihre Einträge (map.entries()) gruppiert werden, wobei der Callback das [key, value]-Tupel erhält. Diese Flexibilität macht Object.groupBy und Map.groupBy zu universellen Werkzeugen für beliebige Iterable-Quellen in der modernen JavaScript-Entwicklung.
// Object.groupBy works on any iterable, not just arrays
const tags = new Set(["JavaScript", "TypeScript", "Java", "CSS", "C++", "JSX"]);
// Group Set elements by first letter — no Array.from() needed
const byLetter = Object.groupBy(tags, tag => tag[0].toUpperCase());
console.log(byLetter["J"]); // ["JavaScript", "Java", "JSX"]
console.log(byLetter["C"]); // ["CSS", "C++"]
// Grouping generator output directly
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
const grouped = Object.groupBy(range(1, 10), n => n % 2 === 0 ? "even" : "odd");
console.log(grouped.even); // [2, 4, 6, 8, 10]
console.log(grouped.odd); // [1, 3, 5, 7, 9]
// TypeScript: typed usage with generics
interface Product { name: string; price: number; category: string; }
function groupProducts(products: Product[]) {
// Return type is inferred: Record<string, Product[]>
return Object.groupBy(products, p => p.category);
}
9. Browserkompatibilität und Polyfill-Strategie
Object.groupBy und Map.groupBy sind seit Chrome 117 (August 2023), Firefox 119 (Oktober 2023) und Safari 17.4 (März 2024) nativ unterstützt. Node.js unterstützt beide Methoden ab Version 21. Für aktuelle Projekte mit einem modernen Browsertarget (z. B. letzte zwei Versionen) können beide Methoden ohne Polyfill verwendet werden. Wer ältere Browser oder Node.js 18 unterstützen muss, benötigt einen Polyfill.
Ein einfacher Polyfill für Object.groupBy ist schnell geschrieben und muss keine externe Abhängigkeit sein: Man prüft, ob Object.groupBy bereits definiert ist, und definiert es andernfalls als Funktion, die intern reduce() verwendet. Für den Produktionseinsatz empfiehlt sich jedoch core-js als umfassende Polyfill-Bibliothek, die beide Methoden korrekt und edge-case-sicher implementiert und über Babel oder Vite automatisch eingebunden werden kann. Mit einem Browserslist-Target wie > 0.5%, last 2 years, not dead wird der Polyfill in den meisten modernen Projekten nicht mehr benötigt.
Mironsoft
Moderne JavaScript-Entwicklung für skalierbare Web-Anwendungen
Lodash-Abhängigkeiten aus dem Projekt entfernen?
Wir analysieren euren JavaScript-Code auf überflüssige Abhängigkeiten und migrieren Lodash-Aufrufe auf native ES2024-Methoden – für weniger Bundle-Größe und zukunftssicheren Code.
Code-Audit
Systematische Analyse auf Lodash-Abhängigkeiten und native Alternativen
Migration
Schrittweise Umstellung auf Object.groupBy, Map.groupBy und weitere ES2024-APIs
Polyfill-Setup
Browserslist und core-js korrekt konfigurieren für optimale Kompatibilität
10. Zusammenfassung
Object.groupBy und Map.groupBy sind zwei der nützlichsten Ergänzungen in ES2024 für den JavaScript-Alltag. Sie lösen das häufige Problem der Array-Gruppierung nativ, ohne externe Bibliotheken und ohne den Boilerplate einer reduce()-Kette. Der Hauptunterschied liegt im Rückgabetyp: Object.groupBy liefert ein Null-Prototyp-Objekt mit String-Schlüsseln und ist damit ideal für einfache, JSON-kompatible Strukturen. Map.groupBy liefert eine echte Map und ermöglicht beliebige Schlüsseltypen inklusive Objekte – unverzichtbar für Domänenmodelle mit komplexen Schlüsseln.
Beide Methoden akzeptieren jedes Iterable, sind vollständig browserkompatibel in modernen Umgebungen und ermöglichen saubere TypeScript-Typisierung ohne zusätzliche Generics. Wer heute noch _.groupBy() aus Lodash importiert, sollte prüfen, ob die native Variante ausreicht – in den meisten Fällen tut sie es, und der Schritt zur Lodash-freien Codebase beginnt genau hier.
Object.groupBy und Map.groupBy — Das Wichtigste auf einen Blick
Object.groupBy
Gibt ein Null-Prototyp-Objekt zurück. Schlüssel sind Strings oder Symbols. Ideal für JSON-kompatible Ergebnisstrukturen und einfache Lookups.
Map.groupBy
Gibt eine Map zurück. Schlüssel können beliebige Werte sein – auch Objekte. Iterationsreihenfolge streng nach Insertion. Ideal für Domänenmodelle.
Iterable-Support
Beide Methoden akzeptieren jedes Iterable: Array, Set, Map, Generator, String. Kein Array.from() als Vorschritt nötig.
Kompatibilität
Chrome 117+, Firefox 119+, Safari 17.4+, Node.js 21+. Für ältere Targets: core-js-Polyfill via Babel oder Vite-Plugin einbinden.