JavaScript 的新陣列分組方法

發表於2024-02-22

對陣列中的專案進行分組,你可能已經做過很多次了。每次都會手動編寫一個分組函式,或者使用 lodashgroupBy 函式。

好訊息是,JavaScript 現在有了分組方法,所以你再也不必這樣做了。Object.groupByMap.groupBy 這兩個新方法將使分組變得更簡單,並節省我們的時間或依賴性。

以前的做法

假設你有一個代表人的物件陣列,你想按年齡對它們進行分組。你可以這樣使用 forEach 迴圈:

const people = [
  { name: "Alice", age: 28 },
  { name: "Bob", age: 30 },
  { name: "Eve", age: 28 },
];

const peopleByAge = {};

people.forEach((person) => {
  const age = person.age;
  if (!peopleByAge[age]) {
    peopleByAge[age] = [];
  }
  peopleByAge[age].push(person);
});
console.log(peopleByAge);
/*
{
  "28": [{"name":"Alice","age":28}, {"name":"Eve","age":28}],
  "30": [{"name":"Bob","age":30}]
}
*/

或者可以像這樣來使用reduce

const peopleByAge = people.reduce((acc, person) => {
  const age = person.age;
  if (!acc[age]) {
    acc[age] = [];
  }
  acc[age].push(person);
  return acc;
}, {});

無論哪種方法,程式碼都略顯笨拙。你總是要檢查物件是否存在分組鍵,如果不存在,就用一個空陣列來建立它。然後再將專案推入陣列。

使用Object.groupBy

有了新的 Object.groupBy 方法,你就可以像這樣得出結果:

const peopleByAge = Object.groupBy(people, (person) => person.age);

簡單多了!不過也有一些需要注意的地方。

Object.groupBy 返回一個空原型物件。這意味著該物件不繼承 Object.prototype 的任何屬性。這很好,因為這意味著你不會意外覆蓋 Object.prototype 上的任何屬性,但這也意味著該物件沒有你可能期望的任何方法,如 hasOwnPropertytoString

const peopleByAge = Object.groupBy(people, (person) => person.age);
console.log(peopleByAge.hasOwnProperty("28"));
// TypeError: peopleByAge.hasOwnProperty is not a function

傳遞給 Object.groupBy 的回撥函式應返回字串或Symbol。如果返回其他內容,則將強制轉為字串。

在我們的示例中,我們一直以數字形式返回age,但在結果中卻被強制轉為字串。儘管如此,你仍然可以使用數字訪問屬性,因為使用方括號符號也會將引數強制為字串。

console.log(peopleByAge[28]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
console.log(peopleByAge["28"]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]

使用Map.groupBy

除了返回 Map 之外,Map.groupBy 的功能與 Object.groupBy 幾乎相同。這意味著你可以使用所有常用的 Map 函式。這也意味著你可以從回撥函式返回任何型別的值。

const ceo = { name: "Jamie", age: 40, reportsTo: null };
const manager = { name: "Alice", age: 28, reportsTo: ceo };

const people = [
  ceo,
  manager,
  { name: "Bob", age: 30, reportsTo: manager },
  { name: "Eve", age: 28, reportsTo: ceo },
];

const peopleByManager = Map.groupBy(people, (person) => person.reportsTo);

在本例中,我們是按照向誰彙報工作來對人員進行分組的。請注意,要從該 Map 中按物件檢索專案,物件必須具有相同的引用。

peopleByManager.get(ceo);
// => [{ name: "Alice", age: 28, reportsTo: ceo }, { name: "Eve", age: 28, reportsTo: ceo }]
peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined

在上面的示例中,第二行使用了一個看起來像 ceo 物件的物件,但它並不是同一個物件,因此它不會從 Map 中返回任何內容。要想成功地從 Map 中獲取專案,請確保你保留了要用作鍵的物件的引用。

何時可用

這兩個 groupBy 方法是 TC39 提議的一部分,目前處於第三階段。這意味著它很有可能成為一項標準,因此也出現了一些實施方案。

Chrome 瀏覽器 117 版本剛剛推出了對這兩種方法的支援,而 Firefox 瀏覽器 119 版本也釋出了對這兩種方法的支援。Safari 以不同的名稱實現了這些方法,我相信他們很快就會更新。既然 Chrome 瀏覽器中出現了這些方法,就意味著它們已在 V8 中實現,因此下次 V8 更新時,Node 中也會出現這些方法。

為什麼使用靜態方法

你可能會問,為什麼要以 Object.groupBy 而不是 Array.prototype.groupBy 的形式來實現呢?根據該提案,有一個庫曾經用一個不相容的 groupBy 方法對 Array.prototype 進行了猴子補丁。在考慮新的應用程式介面時,向後相容性非常重要。幾年前,在嘗試實現 Array.prototype.flatten 時,這一點在一次被稱為 SmooshGate 的事件中得到了強調。

幸運的是,使用靜態方法似乎更有利於未來的可擴充套件性。當 Record 和 Tuples 提議實現時,我們可以新增一個 Record.groupBy 方法,用於將陣列分組為不可變的記錄。

總結

將專案分組顯然是我們開發人員的一項重要工作。目前,每週從 npm 下載 lodash.groupBy 的次數在 150 萬到 200 萬之間。很高興看到 JavaScript 填補了這些空白,讓我們的工作變得更加輕鬆。

現在,下載 Chrome 117 並親自嘗試這些新方法吧。

相關文章