前端基礎

-

JS Object.groupBy 教學 - 再也不用手寫分組邏輯

this.web

groupBy 背景介紹

對資料進行分組是日常開發中常見的需求。就連 Leetcode 都專門出了一題演算法 Group By - LeetCode,就能知道其重要性。

leetcode - groupBy

然而,JS 過去並沒有內建的方法來專門處理分組的問題,開發者通常需要使用 reduce 或其他方法實現。

但好消息是,Object.groupBy 在 ECMAScript 2023 (ES14) 終於被引入作為分組邏輯的解決方案!

沒有 groupBy 之前的做法 - Reduce

Object.groupBy 出現之前,資料分組通常需要通過 Array.prototype.reduce 手動實現。

需要我們自行撰寫邏輯,常常寫出複雜或難以維護的程式碼。下面就來示範一下。

假設我們現在有一組 fruit 資料有 categoryname,我們希望依照 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' }
//   ]
// }

傳統方式的缺點

  1. 程式碼冗長:需要初始化累加器並判斷鍵是否存在等額外步驟。
  2. 可讀性低:對於不熟悉 reduce 的工程師來說,程式碼意圖不夠明確,就會難以閱讀。
  3. 易出錯:手動操作 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:會員等級(可能是 VIPGOLDSILVER 等)
  • purchaseAmount:這個月的消費金額
  • target:這個月的目標消費金額

我們現在要根據兩個目標來幫會員分組:

  1. 先根據 level 分組
  2. 檢查他們本月 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 去進行分組。

支援度也還不錯,可以在內部專案做使用了~

object.groupBy 支援度

你可能會感興趣的文章 👇