Array Grouping in JavaScript
Array grouping is a feature that allows you to organize array elements into groups based on a specified criterion. The Object.groupBy
and Map.groupBy
methods were introduced in ECMAScript 2023 to provide native support for this common operation.
Object.groupBy
The Object.groupBy
method creates an object whose keys are determined by a callback function.
const inventory = [
{ name: "apple", type: "fruit", quantity: 5 },
{ name: "banana", type: "fruit", quantity: 10 },
{ name: "carrot", type: "vegetable", quantity: 8 },
{ name: "lettuce", type: "vegetable", quantity: 3 }
];
// Group by type
const groupedByType = Object.groupBy(inventory, item => item.type);
console.log(groupedByType);
// {
// fruit: [
// { name: "apple", type: "fruit", quantity: 5 },
// { name: "banana", type: "fruit", quantity: 10 }
// ],
// vegetable: [
// { name: "carrot", type: "vegetable", quantity: 8 },
// { name: "lettuce", type: "vegetable", quantity: 3 }
// ]
// }
Map.groupBy
The Map.groupBy
method works similarly but returns a Map object, which can have keys of any type.
// Group by quantity threshold
const groupedByQuantity = Map.groupBy(inventory, item =>
item.quantity > 5 ? "high" : "low"
);
console.log(groupedByQuantity);
// Map {
// "low": [
// { name: "apple", type: "fruit", quantity: 5 },
// { name: "lettuce", type: "vegetable", quantity: 3 }
// ],
// "high": [
// { name: "banana", type: "fruit", quantity: 10 },
// { name: "carrot", type: "vegetable", quantity: 8 }
// ]
// }
Using Non-String Keys with Map.groupBy
Unlike Object.groupBy
, Map.groupBy
can use non-string keys:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Group by remainder when divided by 3
const groupedByRemainder = Map.groupBy(numbers, num => num % 3);
console.log(groupedByRemainder);
// Map {
// 0: [3, 6, 9],
// 1: [1, 4, 7, 10],
// 2: [2, 5, 8]
// }
Traditional Approaches (Pre-ES2023)
Before these methods were available, developers used manual approaches:
Using Reduce
function groupBy(array, keyFn) {
return array.reduce((result, item) => {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
return result;
}, {});
}
// Usage
const groupedByType = groupBy(inventory, item => item.type);
Using forEach
function groupBy(array, keyFn) {
const result = {};
array.forEach(item => {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
});
return result;
}
Using Map with Reduce
function mapGroupBy(array, keyFn) {
return array.reduce((result, item) => {
const key = keyFn(item);
if (!result.has(key)) {
result.set(key, []);
}
result.get(key).push(item);
return result;
}, new Map());
}
Practical Applications
1. Data Analysis
const sales = [
{ product: "Laptop", region: "North", amount: 1200 },
{ product: "Phone", region: "North", amount: 800 },
{ product: "Laptop", region: "South", amount: 1500 },
{ product: "Phone", region: "South", amount: 750 },
{ product: "Tablet", region: "North", amount: 600 },
{ product: "Laptop", region: "East", amount: 1300 }
];
// Group sales by product
const salesByProduct = Object.groupBy(sales, item => item.product);
// Calculate total sales by product
const totalByProduct = Object.entries(salesByProduct).map(([product, items]) => ({
product,
total: items.reduce((sum, item) => sum + item.amount, 0)
}));
console.log(totalByProduct);
// [
// { product: "Laptop", total: 4000 },
// { product: "Phone", total: 1550 },
// { product: "Tablet", total: 600 }
// ]
2. UI Organization
const users = [
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" },
{ id: 3, name: "Charlie", role: "user" },
{ id: 4, name: "Diana", role: "admin" },
{ id: 5, name: "Eve", role: "moderator" }
];
// Group users by role for UI display
const usersByRole = Object.groupBy(users, user => user.role);
// Render users grouped by role
function renderUserGroups(groups) {
let html = '';
for (const [role, users] of Object.entries(groups)) {
html += `<h2>${role.toUpperCase()}</h2><ul>`;
for (const user of users) {
html += `<li>${user.name}</li>`;
}
html += '</ul>';
}
return html;
}
3. Data Transformation
const events = [
{ date: "2023-01-15", type: "login", user: "alice" },
{ date: "2023-01-15", type: "purchase", user: "bob" },
{ date: "2023-01-16", type: "login", user: "charlie" },
{ date: "2023-01-16", type: "purchase", user: "alice" },
{ date: "2023-01-17", type: "login", user: "bob" }
];
// Group events by date
const eventsByDate = Object.groupBy(events, event => event.date);
// Transform to calendar format
const calendar = Object.entries(eventsByDate).map(([date, events]) => ({
date,
eventCount: events.length,
users: [...new Set(events.map(event => event.user))]
}));
console.log(calendar);
// [
// { date: "2023-01-15", eventCount: 2, users: ["alice", "bob"] },
// { date: "2023-01-16", eventCount: 2, users: ["charlie", "alice"] },
// { date: "2023-01-17", eventCount: 1, users: ["bob"] }
// ]
Advanced Grouping Techniques
Multi-level Grouping
const transactions = [
{ date: "2023-01-15", category: "food", amount: 25 },
{ date: "2023-01-15", category: "transport", amount: 15 },
{ date: "2023-01-16", category: "food", amount: 30 },
{ date: "2023-01-16", category: "entertainment", amount: 45 }
];
// First group by date
const byDate = Object.groupBy(transactions, t => t.date);
// Then group each date's transactions by category
const byDateAndCategory = Object.fromEntries(
Object.entries(byDate).map(([date, items]) => [
date,
Object.groupBy(items, item => item.category)
])
);
console.log(byDateAndCategory);
// {
// "2023-01-15": {
// "food": [{ date: "2023-01-15", category: "food", amount: 25 }],
// "transport": [{ date: "2023-01-15", category: "transport", amount: 15 }]
// },
// "2023-01-16": {
// "food": [{ date: "2023-01-16", category: "food", amount: 30 }],
// "entertainment": [{ date: "2023-01-16", category: "entertainment", amount: 45 }]
// }
// }
Custom Grouping Logic
const scores = [85, 92, 78, 65, 98, 72, 88, 91];
// Group by grade
const byGrade = Map.groupBy(scores, score => {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
});
console.log(byGrade);
// Map {
// "A": [92, 98, 91],
// "B": [85, 88],
// "C": [78, 72],
// "D": [65]
// }
Performance Considerations
The native Object.groupBy
and Map.groupBy
methods are optimized for performance:
- Time Complexity: O(n) where n is the array length
- Memory Usage: Creates a new object/map with references to original array elements
- Efficiency: More efficient than manual implementations, especially for large arrays
Browser and Environment Support
As of 2023, these methods are relatively new:
- Check Support: Use feature detection before using in production
- Polyfills: Use polyfills for older environments
- Transpilation: Not directly transpilable (needs runtime support)
// Feature detection
if (typeof Object.groupBy !== 'function') {
// Use polyfill or alternative implementation
}
Interview Tips
- Explain the difference between
Object.groupBy
andMap.groupBy
- Describe how to implement grouping manually using
reduce
- Discuss when to use object grouping vs. map grouping
- Explain the performance benefits of native grouping methods
- Demonstrate knowledge of practical applications for array grouping
- Mention that these are relatively new features and discuss backward compatibility approaches
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.