前端基礎
-JS Object.groupBy 教學 - 再也不用手寫分組邏輯
groupBy 背景介紹
對資料進行分組是日常開發中常見的需求。就連 Leetcode 都專門出了一題演算法 Group By - LeetCode,就能知道其重要性。

然而,JS 過去並沒有內建的方法來專門處理分組的問題,開發者通常需要使用 reduce
或其他方法實現。
但好消息是,Object.groupBy
在 ECMAScript 2023 (ES14) 終於被引入作為分組邏輯的解決方案!
沒有 groupBy 之前的做法 - Reduce
在 Object.groupBy
出現之前,資料分組通常需要通過 Array.prototype.reduce
手動實現。
需要我們自行撰寫邏輯,常常寫出複雜或難以維護的程式碼。下面就來示範一下。
假設我們現在有一組 fruit
資料有 category
和 name
,我們希望依照 category
來分組。
如下:
const data = [
{ category: 'fruit', name: 'apple' },
{ category: 'fruit', name: 'banana' },
{ category: 'vegetable', name: 'carrot' },
{ category: 'vegetable', name: 'lettuce' }
];
// 希望變成
// {
// fruit: [
// { category: 'fruit', name: 'apple' },
// { category: 'fruit', name: 'banana' }
// ],
// vegetable: [
// { category: 'vegetable', name: 'carrot' },
// { category: 'vegetable', name: 'lettuce' }
// ]
// }
以往我們會使用 reduce
來實現,但很難一眼就看懂他的邏輯:
const grouped = data.reduce((acc, item) => {
const key = item.category;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(item);
return acc;
}, {});
console.log(grouped);
// {
// fruit: [
// { category: 'fruit', name: 'apple' },
// { category: 'fruit', name: 'banana' }
// ],
// vegetable: [
// { category: 'vegetable', name: 'carrot' },
// { category: 'vegetable', name: 'lettuce' }
// ]
// }
傳統方式的缺點
- 程式碼冗長:需要初始化累加器並判斷鍵是否存在等額外步驟。
- 可讀性低:對於不熟悉
reduce
的工程師來說,程式碼意圖不夠明確,就會難以閱讀。 - 易出錯:手動操作 key 可能導致錯誤,特別是在需要處理多層嵌套數據的時候。
這些問題增加了程式的複雜度,若分組邏輯在複雜一點,就會變得更難維護,增加開發成本。
groupBy 來解決!
而 Object.groupBy
就能很好的解決這個問題,Object.groupBy
的語法非常簡潔且直觀:
Object.groupBy(array, callbackfn);
array
:需要分組的陣列。callbackfn
:分組條件的回調函數,就是分組的邏輯,他會將返回的值將作為 key。
下面簡單做的示範:
const data = ['one', 'two', 'three', 'four'];
const grouped =
Object.groupBy(data, word => word.length);
console.log(grouped);
// {
// 3: ['one', 'two'],
// 4: ['four'],
// 5: ['three']
// }
在這個示範中,回調函數以字符串長度作為分組條件。
透過 Object.groupBy
,我們就可以非常直接地看出資料是如何被分組的,大幅簡化了邏輯並提高了程式碼的可讀性。超讚!
如何用 groupBy 解決前面的問題
要解決前面的問題也非常簡單,我們想根據 category
來分組,那就只要回傳 category
就好!就像以下的程式碼:
const data = [
{ category: 'fruit', name: 'apple' },
{ category: 'fruit', name: 'banana' },
{ category: 'vegetable', name: 'carrot' },
{ category: 'vegetable', name: 'lettuce' }
];
const grouped =
Object.groupBy(data, item => item.category);
console.log(grouped);
// {
// fruit: [
// { category: 'fruit', name: 'apple' },
// { category: 'fruit', name: 'banana' }
// ],
// vegetable: [
// { category: 'vegetable', name: 'carrot' },
// { category: 'vegetable', name: 'lettuce' }
// ]
// }
可以發現程式碼簡潔許多,可讀性也高,讓整體更容易維護和擴展~
Object.groupBy callbackfn 的延伸應用
除了單純的分組,groupBy 的 callback funciton 還能透過多種判斷或組合 object 的 key 來延伸出許多應用,下面舉個例子:
假設我們管理一群會員,每個會員包含:
name
:會員姓名level
:會員等級(可能是VIP
、GOLD
、SILVER
等)purchaseAmount
:這個月的消費金額target
:這個月的目標消費金額
我們現在要根據兩個目標來幫會員分組:
- 先根據
level
分組 - 檢查他們本月
purchaseAmount
是否達到target
,若達標就將分組 key 補上"-metTarget"
,沒達標就補上"-notMet"
。
這種情況若用 reduce
就會變得比較複雜,但使用 groupBy
就簡單許多:
// 資料範例
const members = [
{ name: 'Alice', level: 'VIP', purchaseAmount: 2000, target: 1500 },
{ name: 'Bob', level: 'VIP', purchaseAmount: 1000, target: 1500 },
{ name: 'Carol', level: 'GOLD', purchaseAmount: 3000, target: 3000 },
{ name: 'David', level: 'GOLD', purchaseAmount: 2500, target: 3000 },
{ name: 'Erin', level: 'SILVER', purchaseAmount: 800, target: 500 },
{ name: 'Frank', level: 'SILVER', purchaseAmount: 400, target: 800 },
];
// groupBy callback function
const groupedByLevelAndTarget = members.groupBy(member => {
const baseKey = member.level;
// 是否達標
const suffix = member.purchaseAmount >= member.target ? '-metTarget' : '-notMet';
return baseKey + suffix;
});
console.log(groupedByLevelAndTarget);
/*
{
"VIP-metTarget": [
{ name: 'Alice', level: 'VIP', purchaseAmount: 2000, target: 1500 }
],
"VIP-notMet": [
{ name: 'Bob', level: 'VIP', purchaseAmount: 1000, target: 1500 }
],
"GOLD-metTarget": [
{ name: 'Carol', level: 'GOLD', purchaseAmount: 3000, target: 3000 }
],
"GOLD-notMet": [
{ name: 'David', level: 'GOLD', purchaseAmount: 2500, target: 3000 }
],
"SILVER-metTarget": [
{ name: 'Erin', level: 'SILVER', purchaseAmount: 800, target: 500 }
],
"SILVER-notMet": [
{ name: 'Frank', level: 'SILVER', purchaseAmount: 400, target: 800 }
]
}
*/
callbackfn
的靈活性讓我們可以根據不同的業務需求,更好的寫出分組邏輯,進一步提升開發效率。
Object.groupBy 和 Map.groupBy 的差別
除了 Object.groupBy
之外,Map.groupBy
是另一個類似的分組方法,但它返回的是 Map
而非普通的 Object。
const data = ['one', 'two', 'three', 'four'];
// Object.groupBy
const objectGrouped = Object.groupBy(data, word => word.length);
// Map.groupBy
const mapGrouped = Map.groupBy(data, word => word.length);
console.log(objectGrouped);
// { 3: ['one', 'two'], 5: ['three'], 4: ['four'] }
console.log(mapGrouped);
// Map(3) { 3 => ['one', 'two'], 5 => ['three'], 4 => ['four'] }
Object.groupBy
:適用於需要普通對象的場景,特別是在需要與 JSON 進行交互時非常方便。Map.groupBy
:適用於需要 key 值支持更多類型的場景,並且對於需要保持鍵值順序的情況更為合適。
Map.groupBy 範例:以物件作為 key 進行分組
之前遇過一個情況,需要以物件當成 key 值來分組,在這邊舉一個類似的例子:
- 你有一批「產品物件」,每個物件都有一個供應商(也是另一個物件)作為參照。
- 你想要把所有產品依照「供應商物件的參考」分組,以便後續快速對該供應商的所有產品進行統一操作。
這時候用 Map.groupBy()
(或自行用 reduce
+ Map
)進行分組,就能直接用物件本身當作 key,非常方便。
// 假設你有一群產品,每個產品都有一個 "supplier" (物件)
const supplierA = { name: 'Supplier A', location: 'Taipei' };
const supplierB = { name: 'Supplier B', location: 'Tainan' };
const products = [
{ id: 1, name: 'Product 1', supplier: supplierA },
{ id: 2, name: 'Product 2', supplier: supplierB },
{ id: 3, name: 'Product 3', supplier: supplierA },
// ... 其他產品
];
const groupedBySupplier = Map.groupBy(products, product => product.supplier);
// groupedBySupplier 的 key 為 supplier 物件本身
// 取出 supplierA 對應的群組
console.log(groupedBySupplier.get(supplierA));
若改用 Object.groupBy()
,就沒辦法直接把 supplier
物件當作 key,而必須想辦法轉成字串(例如 JSON.stringify()
),這樣可能會失去供應商「原物件」的參考,或是遇到循環參照時就更棘手。
結尾
Object.groupBy
是 ECMAScript 2023 (ES14) 新增的物件方式,用來處理資料分組,
它大幅簡化了程式的邏輯,讓我們不用再使用 reduce 去進行分組。
支援度也還不錯,可以在內部專案做使用了~
