JavaScript内核与高级编程之:`Array Grouping` 提案:如何使用 `groupBy()` 和 `groupToMap()` 对 `JavaScript` 数组进行分组。

各位听众,大家好!今天咱们来聊聊JavaScript里一个让人期待已久的新特性:Array Grouping,也就是数组分组。相信不少小伙伴在处理数据的时候都遇到过需要把数组按照某种规则进行分组的需求,以前可能得自己写循环、用reduce,各种花式操作。现在好了,有了 groupBy()groupToMap() 这两个好帮手,咱们可以更优雅、更高效地完成任务了。

为什么要分组?生活中的例子告诉你

想象一下,你是一家电商平台的运营,手里有一堆商品数据,需要按照商品类别进行统计分析,比如看看哪个类别的商品卖得最好,哪个类别的库存压力最大。这时候,把商品按照类别分组就显得非常重要了。

再比如,你是一位老师,需要分析学生的成绩,想看看各个分数段的学生人数分布情况。把学生按照分数段分组,就能快速了解整体学习情况。

总之,分组在数据处理中无处不在,有了 groupBy()groupToMap(),咱们就能更好地驾驭数据,让数据为我们所用。

groupBy():分组并返回一个对象

groupBy() 方法会按照你指定的规则,将数组中的元素分组到一个对象中。对象的键是分组的依据,值是属于该组的元素组成的数组。

语法:

Array.prototype.groupBy(callbackFn)
  • callbackFn:一个用来决定元素分组依据的回调函数。这个函数接收三个参数:
    • element:当前正在处理的元素。
    • index:当前元素的索引。
    • array:正在操作的数组。
      callbackFn 的返回值将作为分组的键。

例子:

假设我们有一个包含学生信息的数组:

const students = [
  { name: 'Alice', grade: 'A' },
  { name: 'Bob', grade: 'B' },
  { name: 'Charlie', grade: 'A' },
  { name: 'David', grade: 'C' },
  { name: 'Eve', grade: 'B' },
];

现在,我们想按照学生的成绩等级(grade)进行分组,就可以这样写:

const groupedStudents = students.groupBy(student => student.grade);

console.log(groupedStudents);
// 输出:
// {
//   A: [ { name: 'Alice', grade: 'A' }, { name: 'Charlie', grade: 'A' } ],
//   B: [ { name: 'Bob', grade: 'B' }, { name: 'Eve', grade: 'B' } ],
//   C: [ { name: 'David', grade: 'C' } ]
// }

可以看到,groupBy() 方法返回了一个对象,对象的键是成绩等级(’A’、’B’、’C’),值是对应等级的学生组成的数组。

再来个例子:按照奇偶数分组

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const groupedNumbers = numbers.groupBy(number => number % 2 === 0 ? 'even' : 'odd');

console.log(groupedNumbers);
// 输出:
// {
//   odd: [ 1, 3, 5, 7, 9 ],
//   even: [ 2, 4, 6, 8, 10 ]
// }

这个例子里,我们按照数字的奇偶性进行分组,callbackFn 返回 ‘even’ 或 ‘odd’ 作为分组的键。

groupToMap():分组并返回一个 Map 对象

groupToMap() 方法和 groupBy() 方法类似,也是按照你指定的规则对数组进行分组,但不同的是,groupToMap() 返回的是一个 Map 对象。

为什么要用 Map?

Map 对象相比普通对象有几个优点:

  • 键可以是任意类型: 普通对象的键只能是字符串或 Symbol,而 Map 对象的键可以是任意类型,包括对象、数组、函数等等。
  • 保持插入顺序: Map 对象会记住键值对的插入顺序,这在某些场景下非常有用。
  • 更好的性能: 在某些情况下,Map 对象的性能可能比普通对象更好,尤其是在键的数量非常大的时候。

语法:

Array.prototype.groupToMap(callbackFn)
  • callbackFn:与 groupBy() 方法中的 callbackFn 用法相同。

例子:

还是用上面的学生信息数组:

const students = [
  { name: 'Alice', grade: 'A' },
  { name: 'Bob', grade: 'B' },
  { name: 'Charlie', grade: 'A' },
  { name: 'David', grade: 'C' },
  { name: 'Eve', grade: 'B' },
];

groupToMap() 按照成绩等级分组:

const groupedStudentsMap = students.groupToMap(student => student.grade);

console.log(groupedStudentsMap);
// 输出:
// Map(3) {
//   'A' => [ { name: 'Alice', grade: 'A' }, { name: 'Charlie', grade: 'A' } ],
//   'B' => [ { name: 'Bob', grade: 'B' }, { name: 'Eve', grade: 'B' } ],
//   'C' => [ { name: 'David', grade: 'C' } ]
// }

可以看到,groupToMap() 方法返回了一个 Map 对象,键是成绩等级,值是对应等级的学生组成的数组。

Map 的优势:键可以是对象

假设我们有一个包含商品信息的数组,每个商品都有一个 category 属性,这个属性是一个对象,包含 idname

const products = [
  { name: 'Laptop', category: { id: 1, name: 'Electronics' } },
  { name: 'T-shirt', category: { id: 2, name: 'Clothing' } },
  { name: 'Mouse', category: { id: 1, name: 'Electronics' } },
  { name: 'Jeans', category: { id: 2, name: 'Clothing' } },
];

如果我们想按照 category 对象进行分组,用 groupBy() 就会遇到问题,因为对象的键会被自动转换为字符串 "[object Object]",导致所有商品都被分到同一组。

这时候,groupToMap() 的优势就体现出来了:

const groupedProductsMap = products.groupToMap(product => product.category);

console.log(groupedProductsMap);
// 输出:
// Map(2) {
//   { id: 1, name: 'Electronics' } => [ { name: 'Laptop', category: {…} }, { name: 'Mouse', category: {…} } ],
//   { id: 2, name: 'Clothing' } => [ { name: 'T-shirt', category: {…} }, { name: 'Jeans', category: {…} } ]
// }

groupToMap() 正确地按照 category 对象进行了分组。

性能考量

groupBy()groupToMap() 的性能取决于以下因素:

  • 数组的大小: 数组越大,分组所需的时间越长。
  • 分组依据的复杂度: callbackFn 的逻辑越复杂,分组所需的时间越长。
  • 分组的数量: 分组越多,所需的时间可能越长。

一般来说,对于中小规模的数组,groupBy()groupToMap() 的性能都足够好。但对于大规模的数组,可能需要考虑优化 callbackFn 的逻辑,或者使用其他更高效的分组算法。

兼容性问题

截至目前(2024年),groupBy()groupToMap() 提案还在 Stage 3 阶段,这意味着它们还没有被正式纳入 ECMAScript 标准。因此,在某些浏览器或 Node.js 版本中可能无法直接使用。

不过,我们可以使用 polyfill 来解决兼容性问题。Polyfill 是一段代码,它提供了一种在旧环境中实现新特性的方法。

例如,可以使用 core-js 这个库来提供 groupBy()groupToMap() 的 polyfill:

npm install core-js

然后在代码中引入:

import 'core-js/features/array/group-by';
import 'core-js/features/array/group-to-map';

// 就可以使用 groupBy() 和 groupToMap() 了

总结:groupBy() vs groupToMap()

特性 groupBy() groupToMap()
返回值类型 普通对象 Map 对象
键的类型 字符串或 Symbol 任意类型
插入顺序 不保证 保持插入顺序
适用场景 键是字符串或 Symbol,不需要保持插入顺序 键可以是任意类型,或者需要保持插入顺序

高级用法:结合其他数组方法

groupBy()groupToMap() 可以与其他数组方法结合使用,实现更复杂的数据处理逻辑。

例子:先过滤再分组

假设我们只想按照成绩等级对及格的学生进行分组:

const students = [
  { name: 'Alice', grade: 'A', score: 90 },
  { name: 'Bob', grade: 'B', score: 75 },
  { name: 'Charlie', grade: 'A', score: 60 },
  { name: 'David', grade: 'C', score: 50 },
  { name: 'Eve', grade: 'B', score: 80 },
];

const groupedPassingStudents = students
  .filter(student => student.score >= 60) // 先过滤出及格的学生
  .groupBy(student => student.grade); // 再按照成绩等级分组

console.log(groupedPassingStudents);
// 输出:
// {
//   A: [ { name: 'Alice', grade: 'A', score: 90 }, { name: 'Charlie', grade: 'A', score: 60 } ],
//   B: [ { name: 'Bob', grade: 'B', score: 75 }, { name: 'Eve', grade: 'B', score: 80 } ]
// }

例子:分组后统计数量

假设我们想统计每个成绩等级的学生人数:

const students = [
  { name: 'Alice', grade: 'A' },
  { name: 'Bob', grade: 'B' },
  { name: 'Charlie', grade: 'A' },
  { name: 'David', grade: 'C' },
  { name: 'Eve', grade: 'B' },
];

const groupedStudents = students.groupBy(student => student.grade);

const gradeCounts = Object.entries(groupedStudents).map(([grade, students]) => ({
  grade,
  count: students.length,
}));

console.log(gradeCounts);
// 输出:
// [
//   { grade: 'A', count: 2 },
//   { grade: 'B', count: 2 },
//   { grade: 'C', count: 1 }
// ]

总结

groupBy()groupToMap() 是 JavaScript 中非常实用的数组分组方法,它们可以帮助我们更方便、更高效地处理数据。选择哪个方法取决于具体的应用场景,如果键是字符串或 Symbol,并且不需要保持插入顺序,那么 groupBy() 就足够了。如果键可以是任意类型,或者需要保持插入顺序,那么 groupToMap() 则是更好的选择。

希望今天的讲座能帮助大家更好地理解和使用 groupBy()groupToMap(),让它们成为你数据处理工具箱中的利器!

大家有什么问题吗?欢迎提问!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注