•  阅读 3 分钟

JavaScript 对象分组方法 `Object.groupBy()` 和 `Map.groupBy()`

在日常开发中我们经常需要对数组和类数组等可迭代对象按照一定的条件进行分组,现在 JavaScript 支持原生静态方法 Object.groupBy()Map.groupBy()

数据分组

数据分组是代码编程中常见的需求,例如在编写按年份或分类对博客文章分组展示。在过去,可以使用 for 循环或者数组遍历方法 reduce

const posts = [
  { slug: 'post-1', tag: 'js', date: '2024-09-15', content: 'post-1' },
  { slug: 'post-2', tag: 'css', date: '2024-10-03', content: 'post-2' },
  { slug: 'post-3', tag: 'js', date: '2024-10-12', content: 'post-3' },
]

const groupByTag = posts.reduce((group, post) => {
  const { tag } = post
  if (!Array.isArray(group[tag])) {
    group[tag] = []
  }
  group[tag].push(post)

  return group
}, {})

可以看到需要我们自行处理数据结构和分组逻辑。现在我们可以尝试静态方法 Object.groupBy()

Object.groupBy() 方法

Object.groupBy(items, callbackFn)

Object.groupBy() 方法接收两个参数 itemscallbackFn。其中 items 可以是数组或者类数组等可迭代对象,callbackFn 是遍历成员元素处理分组逻辑,其返回值将作为分组对象的键值。每个键值都有一个相关联的数组,其中包含回调函数返回相同值的所有元素。

数组分组

同样是上面的博客文章列表,同样以 tag 值作为分组词:

const groupByTag = Object.groupBy(posts, ({ tag }) => tag)

console.log(groupByTag)
/* 输出:
{
  js: [
    { slug: 'post-1', tag: 'js', date: '2024-09-15', content: 'post-1' },
    { slug: 'post-3', tag: 'js', date: '2024-10-12', content: 'post-3' },
  ],
  css: [
    { slug: 'post-2', tag: 'css', date: '2024-10-03', content: 'post-2' },
  ],
}
*/

可以看到相比数组方法 reduce,静态方法 Object.groupBy() 分组代码更加简洁,可读性更强,语义更明确。

类数组分组

Object.groupBy() 方法可以对字符串、dom 节点等类数组分组:

const groupByStr = Object.groupBy('ABCD', (k, index) => index)

console.log(groupByStr)
// { '0', ['A'], '1': ['B'], '2': ['C'], '3': ['D'] }

可以看到数字索引作为键值时变成数字字符串,Object.groupBy() 的回调函数会对返回值不是字符串或 Symbol 类型的值进行强制类型转换。

Map.groupBy() 方法

静态方法 Map.groupBy() 接受的参数和处理方式与 Object.groupBy() 相同,只是非空返回值类型是 Map

const mapGroupByTag = Map.groupBy(posts, ({ tag }) => tag)

console.log(mapGroupByTag.get('js'))
/* 输出:
[
  { slug: 'post-1', tag: 'js', date: '2024-09-15', content: 'post-1' },
  { slug: 'post-3', tag: 'js', date: '2024-10-12', content: 'post-3' },
]
*/

同时,由于返回值类型是 Map,所以支持的键值类型可以是对象、Number、Boolean:

const dateOct = { month: 9 }
const dateNotOct = { month: 0 }
const mapGroupByOct = Map.groupBy(posts, ({ date }) => {
  return new Date(date).getMonth() === 9 ? dateOct : dateNotOct
})
console.log(mapGroupByOct.get(dateOct))
/* 输出:
[
  { slug: 'post-2', tag: 'css', date: '2024-10-03', content: 'post-2' },
  { slug: 'post-3', tag: 'js', date: '2024-10-12', content: 'post-3' }
]
*/

// Map.groupBy 不对键值进行类型转换,即接受数字作为键值
const mapGroupByStr = Map.groupBy('ABCD', (k, index) => index)
console.log(mapGroupByStr)
// Map(4) { 0 => [ 'A' ], 1 => [ 'B' ], 2 => [ 'C' ], 3 => [ 'D' ] }

浅拷贝对象

方法 Object.groupBy()Map.groupBy() 不是深层拷贝,就是说返回的对象中的元素和原始可迭代对象中的元素相同。更改元素的内部结构将反映在原始可迭代对象和返回的对象中:

const mapGroupByTag = Map.groupBy(posts, ({ tag }) => tag)

console.log(mapGroupByTag.get('js')[0])
// { slug: 'post-1', tag: 'js', date: '2024-09-15', content: 'post-1' } // posts[0]

// 尝试更改分组对象
mapGroupByTag.get('js')[0].tag = 'css'
// 原始对象也会被更改
console.log(posts[0])
// { slug: 'post-1', tag: 'css', date: '2024-09-15', content: 'post-1' }

总结

静态方法 Object.groupBy()Map.groupBy() 可以很方便对数组或者类数组等可迭代对象进行分组。方法使用代码简洁,可读性强。方法返回的分组对象是原始对象的浅拷贝,进行写值操作是需要注意。

标签:
> cd ..