导读:本期聚焦于小伙伴创作的《如何在JavaScript中根据多个属性对数组对象进行分组》,敬请观看详情。把订单列表先按城市归类、再按状态拆分,是后台报表里最常见的需求之一。如果只用单层reduce配合一个字符串键,遇到嵌套维度时代码会迅速膨胀且难以维护。更稳妥的做法是利用复合键或逐级归并:把多个字段拼成唯一标识,或者依次按属性建立树状结构。前者实现简单、适合扁平统计;后者保留层级、方便前端递归渲染。下面以销售数据为例,演示如何用reduce与Map处理多字段分组,并对比两种方案在可读性与扩展性上的差异,帮你少写无效循环。

在业务开发中,我们经常会拿到一组结构相似的对象数组,需要根据两个或更多字段把它们归拢成不同的集合。例如一条销售记录包含城市、渠道、金额等字段,运营希望先按城市划分,再在每个城市里按渠道统计。原生JavaScript并没有提供像SQL里GROUP BY那样的多列分组语法,因此必须自己用数组方法组合实现。

如何在JavaScript中根据多个属性对数组对象进行分组

为什么单属性分组不够用

很多初学者第一次做分组时,会写出类似按一个字段分类的代码:用reduce把数组转成以该字段值为键的对象。这种方式在维度单一时没有问题,但一旦要同时看多个维度,单纯把后面的字段塞进同一个字符串键会让数据关系变扁平,后续想单独取某个城市下的所有渠道就很麻烦。

更重要的是,如果分组字段里可能出现特殊字符(比如城市名带点号),直接拼接字符串容易引发键名冲突或歧义。因此在多属性场景下,我们需要明确两种思路:复合键扁平分组,以及逐级嵌套分组。它们适用场景不同,下面分别说明。

方案一:使用复合键进行扁平分组

复合键的思路是,把多个属性值按顺序组合成一个唯一字符串,作为reduce累计对象的键。这样最终得到的是一个一层对象,每个键代表一种属性组合,对应的值是该组合下的对象数组。代码直观,适合导出Excel或做扁平统计。

下面的例子以城市与渠道两个字段分组。我们用JSON序列化或手动拼接来生成键,并借助Map避免重复创建空数组的判断逻辑。注意代码里所有小于号都做了转义,保证在HTML中安全展示。

const sales = [
  { city: '北京', channel: '线上', amount: 100 },
  { city: '北京', channel: '线下', amount: 200 },
  { city: '上海', channel: '线上', amount: 150 },
  { city: '北京', channel: '线上', amount: 50 }
];

function groupByMultiKey(list, keys) {
  const map = new Map();
  for (const item of list) {
    const compositeKey = keys.map(k => item[k]).join('|');
    if (!map.has(compositeKey)) {
      map.set(compositeKey, []);
    }
    map.get(compositeKey).push(item);
  }
  return Object.fromEntries(map);
}

const result = groupByMultiKey(sales, ['city', 'channel']);
console.log(result);
// 输出: { '北京|线上': [...], '北京|线下': [...], '上海|线上': [...] }

这种写法的优点是逻辑简单,分组结果可直接遍历。缺点是丢失了层级语义:如果你想知道“北京”下共有哪些渠道,需要自己再解析键名。当分组字段超过三个时,键名也会变得很长。

方案二:逐级嵌套分组保留树状结构

如果前端需要按层级渲染(如树形表格),更合理的是建立嵌套对象:第一层是城市,第二层是渠道,叶子节点才是数组。我们可以通过递归或累进reduce来实现,让每一级都对应一个子对象。

下面演示一个通用的多属性嵌套分组函数,它接收字段数组,依次下沉。这样无论传两个还是三个字段都能复用,而且读取某城市数据就是result['北京'],结构清晰。

function groupByNested(list, keys) {
  return list.reduce((acc, item) => {
    let current = acc;
    keys.forEach((key, index) => {
      const val = item[key];
      if (index === keys.length - 1) {
        if (!current[val]) current[val] = [];
        current[val].push(item);
      } else {
        if (!current[val]) current[val] = {};
        current = current[val];
      }
    });
    return acc;
  }, {});
}

const tree = groupByNested(sales, ['city', 'channel']);
console.log(tree['北京']['线上']);
// 输出: [{ city: '北京', channel: '线上', amount: 100 }, { city: '北京', channel: '线上', amount: 50 }]

嵌套方案在可读性和扩展性上更好,尤其适合多维分析。但它的结果不是扁平映射,如果后端只接受一维分组格式,还需要额外展开。实际项目中可以根据消费端需求选择。

使用Map与对象的选择

在上面的示例中,复合键方案先用Map存中间状态,最后转成普通对象。这是因为Maphasset语义明确,不必担心原型链污染。如果确定键都是字符串且数量不大,直接用空对象累加也可以,但要注意不要意外覆盖像toString这样的默认属性。

另外,当属性值本身是小写数字或_symbol之类非常规字符串时,用Map能避免隐式类型转换带来的键错位。因此在中大型项目里,推荐以Map做分组中间容器,输出时再视情况转换。

性能与注意事项

两种方案的时间复杂度都是O(n*m),其中n是数组长度,m是分组字段数,属于一次性线性遍历,性能足够应付绝大多数前台报表数据。若数据量达到十万级以上,可以考虑用for循环替代reduce以减少函数调用开销,但代码结构会稍显冗长。

需要提醒的是,分组前最好对参与分组的字段做空值兜底。比如某条记录缺少channel属性,直接取item['channel']会得到undefined,拼进键或嵌套层时可能产生“undefined”分组。建议在函数开头用|| '未知'之类的默认值处理,保证分组结果完整。

总结

在JavaScript里根据多个属性对数组对象分组,核心就是决定数据落地形态:扁平复合键适合简单统计与导出,嵌套结构适合层级展示。借助reduceMap以及合理的默认值处理,就能用少量代码覆盖日常绝大部分分组需求,而不必引入额外库。

JavaScript数组分组对象属性修改时间:2026-08-01 10:39:28

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。