如何按键将对象数组分组?

402

有没有人知道一种方法(最好用lodash),可以根据对象的一个键将对象数组分组,然后基于这个分组创建一个新的对象数组?例如,我有一个汽车对象数组:

const cars = [
    {
        'make': 'audi',
        'model': 'r8',
        'year': '2012'
    }, {
        'make': 'audi',
        'model': 'rs5',
        'year': '2013'
    }, {
        'make': 'ford',
        'model': 'mustang',
        'year': '2012'
    }, {
        'make': 'ford',
        'model': 'fusion',
        'year': '2015'
    }, {
        'make': 'kia',
        'model': 'optima',
        'year': '2012'
    },
];

我想创建一个按 制造商 分组的新汽车对象数组:

const cars = {
    'audi': [
        {
            'model': 'r8',
            'year': '2012'
        }, {
            'model': 'rs5',
            'year': '2013'
        },
    ],

    'ford': [
        {
            'model': 'mustang',
            'year': '2012'
        }, {
            'model': 'fusion',
            'year': '2015'
        }
    ],

    'kia': [
        {
            'model': 'optima',
            'year': '2012'
        }
    ]
}

4
你的结果无效。 - Nina Scholz
有没有类似的方法可以获取一个 Map 而不是一个对象? - Andrea Bergonzo
3
如果您正在使用Typescript(这不是OP的情况),那么您已经具有groupBy方法。 您可以通过 your_array.groupBy(...) 进行使用。 - Isac Moura
9
你的数组没有 "groupBy(...)" 这个方法! - Martijn Hiemstra
这个人创建了一个版本的beta js函数array.group(),它的效果非常棒: https://dmitripavlutin.com/javascript-array-group/ - Facundo Colombier
@NinaScholz 对不起,但他们的期望结果有什么问题吗? - wynx
34个回答

546
在纯粹的Javascript中,你可以使用Array#reduce与一个对象。

var cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }],
    result = cars.reduce(function (r, a) {
        r[a.make] = r[a.make] || [];
        r[a.make].push(a);
        return r;
    }, Object.create(null));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

2023年更新

现在新增了Object.groupBy功能。它接受一个可迭代对象和一个用于分组的函数。

var cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }],
    result = Object.groupBy(cars, ({ make }) => make);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }


3
我该如何迭代“result”结果? - Mounir Elfassi
3
你可以使用Object.entries获取对象的键/值对,并循环遍历它们。 - Nina Scholz
4
为什么要使用 Object.create(null)?这是最佳答案。 - kartik
1
@Sarah,如果属性a.make不存在(不存在的属性值为undefined),它将生成一个新的数组属性。如果存在,则将其分配给自身。 - Nina Scholz
1
@Alex,请看这里:https://dev59.com/Lpjga4cB1Zd3GeqPJ23u#38068553 - Nina Scholz
显示剩余18条评论

200

Timo的回答是我会这样做的。使用简单的_.groupBy,并允许在分组结构中的对象中存在一些重复。

然而,OP还要求删除重复的make键。如果你想走到底:

var grouped = _.mapValues(_.groupBy(cars, 'make'),
                          clist => clist.map(car => _.omit(car, 'make')));

console.log(grouped);

产生:

{ audi:
   [ { model: 'r8', year: '2012' },
     { model: 'rs5', year: '2013' } ],
  ford:
   [ { model: 'mustang', year: '2012' },
     { model: 'fusion', year: '2015' } ],
  kia: 
   [ { model: 'optima', year: '2012' } ] 
}

如果您想使用Underscore.js来完成此操作,请注意它的_.mapValues版本被称为_.mapObject


优雅的解决方案! - JSTR

153

您正在寻找_.groupBy()

如果需要,从对象中删除用于分组的属性应该很容易:

const cars = [{
  'make': 'audi',
  'model': 'r8',
  'year': '2012'
}, {
  'make': 'audi',
  'model': 'rs5',
  'year': '2013'
}, {
  'make': 'ford',
  'model': 'mustang',
  'year': '2012'
}, {
  'make': 'ford',
  'model': 'fusion',
  'year': '2015'
}, {
  'make': 'kia',
  'model': 'optima',
  'year': '2012'
}];

const grouped = _.groupBy(cars, car => car.make);

console.log(grouped);
<script src='https://cdn.jsdelivr.net/lodash/4.17.2/lodash.min.js'></script>


40
如果你想更简短一些,可以使用 var grouped = _.groupBy(cars, 'make');。如果访问器是一个简单的属性名,就不需要使用函数了。 - Jonathan Eunice
1
'_' 代表什么? - Adrian Grzywaczewski
@AdrianGrzywaczewski 这是命名空间“lodash”或“underscore”的默认约定。现在,由于库已经模块化,不再需要这样做。例如:https://www.npmjs.com/package/lodash.groupby - vilsbole
14
怎样才能在结果中进行迭代? - Luis Antonio Pestana
2
我相信这应该是使用Object.keys(grouped)。 - Jaqen H'ghar

138

完全没有必要像上面的解决方案建议的那样下载第三方库来解决这个简单的问题。

在es6中,将对象列表按特定键分组的一行版本:

const groupByKey = (list, key) => list.reduce((hash, obj) => ({...hash, [obj[key]]:( hash[obj[key]] || [] ).concat(obj)}), {})

过滤掉没有 key 的对象的更长版本:

function groupByKey(array, key) {
   return array
     .reduce((hash, obj) => {
       if(obj[key] === undefined) return hash; 
       return Object.assign(hash, { [obj[key]]:( hash[obj[key]] || [] ).concat(obj)})
     }, {})
}


var cars = [{'make':'audi','model':'r8','year':'2012'},{'make':'audi','model':'rs5','year':'2013'},{'make':'ford','model':'mustang','year':'2012'},{'make':'ford','model':'fusion','year':'2015'},{'make':'kia','model':'optima','year':'2012'}];

console.log(groupByKey(cars, 'make'))

注意:原始问题似乎询问如何按制造商分组汽车,但在每个组中省略制造商。因此,没有第三方库的简短答案看起来像这样:

const groupByKey = (list, key, {omitKey=false}) => list.reduce((hash, {[key]:value, ...rest}) => ({...hash, [value]:( hash[value] || [] ).concat(omitKey ? {...rest} : {[key]:value, ...rest})} ), {})

var cars = [{'make':'audi','model':'r8','year':'2012'},{'make':'audi','model':'rs5','year':'2013'},{'make':'ford','model':'mustang','year':'2012'},{'make':'ford','model':'fusion','year':'2015'},{'make':'kia','model':'optima','year':'2012'}];

console.log(groupByKey(cars, 'make', {omitKey:true}))


这绝对不是 ES5。 - Shinigami
1
它只是有效地工作!有人能详细解释一下这个reduce函数吗? - Jeevan
我喜欢你们两个的回答,但我发现它们都将“make”字段作为每个“make”数组的成员提供。我根据你们的答案提供了一个答案,其中交付的输出与预期的输出相匹配。谢谢! - Daniel Vukasovich
@Jeevan reduce 的参数是一个回调函数和一个初始值。回调函数有两个参数,分别是前一个值和当前值。在这里,前一个值被称为 hash。也许有人可以更详细地解释它在这里的用法。似乎 reduce 在这里通过提取属性来减少数组的大小。 - Timo
对于考虑使用此代码的任何人,请小心。该代码未针对大型数据集进行优化,如果您有一个要按组分组的大型数组,则可能会遇到问题。我有一个包含33000个项目的数组,使用此方法分组需要近30秒的时间。我相当确定问题主要在reducer中使用的{...hash},它创建了大量未使用的对象,浪费空间,导致过度内存使用和垃圾回收。 - Glen Keane
这个选项的性能特征非常糟糕(二次时间复杂度),而且可读性也不强。它实际上是绝对不应该被使用的。 - undefined

32

这里是您自己的groupBy函数,它是从以下代码进行泛化而来:https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore

function groupBy(xs, f) {
  return xs.reduce((r, v, i, a, k = f(v)) => ((r[k] || (r[k] = [])).push(v), r), {});
}

const cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }];

const result = groupBy(cars, (c) => c.make);
console.log(result);


1
我喜欢这个答案,因为你也可以使用嵌套属性 - 非常好。我只是针对TypeScript进行了更改,它正是我正在寻找的 :) - Alfa Bravo
1
groupBy = (x, f, r = {}) => ( x.forEach(v => (r[f(v)] ??= []).push(v)), r ) - Endless

23

var cars = [{
  make: 'audi',
  model: 'r8',
  year: '2012'
}, {
  make: 'audi',
  model: 'rs5',
  year: '2013'
}, {
  make: 'ford',
  model: 'mustang',
  year: '2012'
}, {
  make: 'ford',
  model: 'fusion',
  year: '2015'
}, {
  make: 'kia',
  model: 'optima',
  year: '2012'
}].reduce((r, car) => {

  const {
    model,
    year,
    make
  } = car;

  r[make] = [...r[make] || [], {
    model,
    year
  }];

  return r;
}, {});

console.log(cars);


需要紧急帮助,如何存储类似于[{“audi”:[{"model":"r8","year":"2012"}]},{“ford”:[{"model":"r9","year":"2021"}]}...]中的每个对象。 - Ravi Sharma

23

使用简单的for循环也是可能的:

 const result = {};

 for(const {make, model, year} of cars) {
   if(!result[make]) result[make] = [];
   result[make].push({ model, year });
 }

1
而且可能更快,更简单。我已经扩展了你的片段,使其更具动态性,因为我有一个来自数据库表的长字段列表,我不想输入。还要注意,您需要将const替换为let。for ( let { TABLE_NAME, ...fields } of source) { result[TABLE_NAME] = result[TABLE_NAME] || []; result[TABLE_NAME].push({ ...fields }); } - adrien
1
TIL,谢谢!https://medium.com/@mautayro/es6-variable-declaration-for-loops-why-const-works-in-a-for-in-loop-but-not-in-a-normal-a200cc5467c2 - adrien

12

我会将 REAL GROUP BY 保留为 JS 数组的示例,就像这个任务 这里 的示例一样。

const inputArray = [ 
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
];

var outObject = inputArray.reduce(function(a, e) {
  // GROUP BY estimated key (estKey), well, may be a just plain key
  // a -- Accumulator result object
  // e -- sequentally checked Element, the Element that is tested just at this itaration

  // new grouping name may be calculated, but must be based on real value of real field
  let estKey = (e['Phase']); 

  (a[estKey] ? a[estKey] : (a[estKey] = null || [])).push(e);
  return a;
}, {});

console.log(outObject);


11

您可以通过 _.groupBy 函数在每次迭代中尝试修改对象。请注意,源数组会更改其元素!

var res = _.groupBy(cars,(car)=>{
    const makeValue=car.make;
    delete car.make;
    return makeValue;
})
console.log(res);
console.log(cars);

1
虽然这段代码可能解决了问题,但附上一个解释说明如何以及为什么解决该问题会有助于提高您的帖子质量。请记住,您是为未来的读者回答问题,而不仅仅是现在提问的人!请编辑您的答案以添加解释,并指出适用的限制和假设。 - Makyen
对我来说,这似乎是最好的答案,因为您只需一次遍历数组即可获得所需的结果。没有必要使用另一个函数来删除“make”属性,而且这样更易读。 - Carrm

9

只需要简单的 forEach 循环,而无需使用任何库即可完成此操作

var cars = [
{
    'make': 'audi',
    'model': 'r8',
    'year': '2012'
}, {
    'make': 'audi',
    'model': 'rs5',
    'year': '2013'
}, {
    'make': 'ford',
    'model': 'mustang',
    'year': '2012'
}, {
    'make': 'ford',
    'model': 'fusion',
    'year': '2015'
}, {
    'make': 'kia',
    'model': 'optima',
    'year': '2012'
},
];
let ObjMap ={};

  cars.forEach(element => {
    var makeKey = element.make;
     if(!ObjMap[makeKey]) {
       ObjMap[makeKey] = [];
     }

    ObjMap[makeKey].push({
      model: element.model,
      year: element.year
    });
   });
   console.log(ObjMap);


1
最优雅和易读的解决方案 - Adeel Shekhani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接