在一个对象数组中进行分组的最有效方法

915

什么是在数组中对对象进行groupby的最有效方法?

例如,给定以下对象数组:

[ 
    { 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" }
]

我正在使用表格展示这些信息。我想按不同的方法进行分组,但是我想对值进行求和。

我正在使用Underscore.js的groupby函数,这很有帮助,但并不能完全满足我的需求,因为我不想让它们“分开”,而是更像SQL中的group by 方法将它们“合并”起来。

我想要的是能够对特定值进行汇总(如果被请求的话)。

所以如果我按Phase 进行分组,我希望收到:

[
    { Phase: "Phase 1", Value: 50 },
    { Phase: "Phase 2", Value: 130 }
]

如果我将 Phase/Step 进行分组,我会收到:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

是否有适用于此的有用脚本,或者我应该坚持使用Underscore.js,然后循环遍历结果对象自己进行总计?


虽然 _.groupBy 本身不能完成工作,但它可以与其他 Underscore 函数结合使用来完成所需的操作,无需手动循环。请参考此回答: https://dev59.com/uGYq5IYBdhLWcg3wfgnd#66112210。 - Julian
更易读的答案版本:function groupBy(data, key){ return data.reduce( (acc, cur) => { acc[cur[key]] = acc[cur[key]] || []; // 如果键是新的,则将其值初始化为数组,否则保留其自己的数组值 acc[cur[key]].push(cur); return acc; } , []) } - aderchox
62个回答

6
假设你有这样一些数据:
[{id:1, cat:'sedan'},{id:2, cat:'sport'},{id:3, cat:'sport'},{id:4, cat:'sedan'}]
通过执行以下代码:
const categories = [...new Set(cars.map((car) => car.cat))]
你将得到以下结果:
['sedan','sport']
解释如下: 1. 首先,我们通过传递一个数组来创建一个新的Set。因为Set只允许唯一的值,所有重复项都将被删除。 2. 现在重复项已经被删除了,我们将使用展开运算符...将其转换回数组。
Set文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set 展开运算符文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

我非常喜欢你的答案,虽然它是最短的,但我仍然不理解逻辑,特别是这里是谁在分组?是展开运算符(...)还是'new Set()'?请向我们解释一下...谢谢。 - Ivan
1
  1. 首先,我们通过传递一个数组来创建一个新的Set。因为Set只允许唯一的值,所有重复项都将被删除。
  2. 现在重复项已经被删除了,我们将使用扩展运算符将其转换回数组... Set文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set 扩展运算符: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
- Yago Gehres

6
你可以使用原生的JavaScript group 数组方法(目前处于第二阶段)。
我认为这个解决方案要比reduce或者依赖第三方库如lodash等更加优雅。

const products = [{
    name: "milk",
    type: "dairy"
  },
  {
    name: "cheese",
    type: "dairy"
  },
  {
    name: "beef",
    type: "meat"
  },
  {
    name: "chicken",
    type: "meat"
  }
];

const productsByType = products.group((product) => product.type);

console.log("Grouped products by type: ", productsByType);
<script src="https://cdn.jsdelivr.net/npm/core-js-bundle@3.23.2/minified.min.js"></script>


1
还有 Array.prototype.groupToMap - Константин Ван

5

根据之前的回答

const groupBy = (prop) => (xs) =>
  xs.reduce((rv, x) =>
    Object.assign(rv, {[x[prop]]: [...(rv[x[prop]] || []), x]}), {});

如果您的环境支持,使用对象展开语法会使代码看起来更加美观。

const groupBy = (prop) => (xs) =>
  xs.reduce((acc, x) => ({
    ...acc,
    [ x[ prop ] ]: [...( acc[ x[ prop ] ] || []), x],
  }), {});

这里,我们的reducer接受部分形成的返回值(从空对象开始),并返回一个由前一个返回值的展开成员和一个新成员组成的对象,其键是根据当前迭代器的prop值计算得出的, 值是该prop的所有值以及当前值的列表。


4

我认为提供的答案没有回答问题,我认为下面的内容应该回答第一个部分:

const arr = [ 
{ 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" }
]

const groupBy = (key) => arr.sort((a, b) => a[key].localeCompare(b[key])).reduce((total, currentValue) => {
  const newTotal = total;
  if (
    total.length &&
    total[total.length - 1][key] === currentValue[key]
  )
    newTotal[total.length - 1] = {
      ...total[total.length - 1],
      ...currentValue,
      Value: parseInt(total[total.length - 1].Value) + parseInt(currentValue.Value),
    };
  else newTotal[total.length] = currentValue;
  return newTotal;
}, []);

console.log(groupBy('Phase'));

// => [{ Phase: "Phase 1", Value: 50 },{ Phase: "Phase 2", Value: 130 }]

console.log(groupBy('Step'));

// => [{ Step: "Step 1", Value: 70 },{ Step: "Step 2", Value: 110 }] 


使用关键字“Step”进行groupBy操作时输出错误。 - CHARFEDDINE Amine
是的,我认为你首先必须对其进行排序:arr.sort((a, b) => a[key] - b[key]).reduce... 我会更新我的回答。 - Aznhar
我的错:sort((a, b) => a[key].localeCompare(b[key])) - Aznhar

4

groupBy函数可以按照特定的键或给定的分组函数将数组分组。带类型。

groupBy = <T, K extends keyof T>(array: T[], groupOn: K | ((i: T) => string)): Record<string, T[]> => {
  const groupFn = typeof groupOn === 'function' ? groupOn : (o: T) => o[groupOn];

  return Object.fromEntries(
    array.reduce((acc, obj) => {
      const groupKey = groupFn(obj);
      return acc.set(groupKey, [...(acc.get(groupKey) || []), obj]);
    }, new Map())
  ) as Record<string, T[]>;
};

我对这个版本(每轮使用新数组和解构来创建要设置的值)与另一个仅在需要时创建空数组的性能基准感兴趣。根据您的代码:https://gist.github.com/masonlouchart/da141b3af477ff04ccc626f188110f28 - Mason
只是为了明确,对于不熟悉的初学者,这是TypeScript代码,而原始问题标记为JavaScript,因此这与主题无关,对吗? - Neek

3

这里是一个ES6版本的代码,不会在空成员上出现错误

function groupBy (arr, key) {
  return (arr || []).reduce((acc, x = {}) => ({
    ...acc,
    [x[key]]: [...acc[x[key]] || [], x]
  }), {})
}

1
这些扩展运算符确实赢得了它们的声誉。 - Rob Lyndon

3
解释同一段代码,这段代码可以在这里找到:here
const groupBy = (array, key) => {
  return array.reduce((result, currentValue) => {
    (result[currentValue[key]] = result[currentValue[key]] || []).push(
      currentValue
    );
    console.log(result);
    return result;
  }, {});
};

使用

 let group =   groupBy(persons, 'color');

3

在重复使用已经编写好的代码(即Underscore)的同时,我们希望全面回答原始问题。如果您结合其100多个函数,您可以做更多事情。以下解决方案演示了这一点。

步骤1:按任意组合的属性将数组中的对象分组。这利用了 _.groupBy 接受返回对象组的函数的事实。它还使用了 _.chain, _.pick, _.values, _.join_.value。请注意,_.value 在此处并非严格必要,因为链接值在用作属性名称时将自动解包。我包含它是为了防止混淆,以防有人尝试在不发生自动解包的情况下编写类似的代码。
// Given an object, return a string naming the group it belongs to.
function category(obj) {
    return _.chain(obj).pick(propertyNames).values().join(' ').value();
}

// Perform the grouping.
const intermediate = _.groupBy(arrayOfObjects, category);

假设原问题中有一个arrayOfObjects,并将propertyNames设置为['Phase', 'Step'],那么intermediate将得到以下值:

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

步骤2:将每个组缩减为单个扁平对象,并在数组中返回结果。除了我们之前看到的函数外,以下代码还使用了_.pluck, _.first, _.pick, _.extend, _.reduce_.map。在这种情况下,_.first保证返回一个对象,因为_.groupBy不会产生空组。此时需要使用_.value
// Sum two numbers, even if they are contained in strings.
const addNumeric = (a, b) => +a + +b;

// Given a `group` of objects, return a flat object with their common
// properties and the sum of the property with name `aggregateProperty`.
function summarize(group) {
    const valuesToSum = _.pluck(group, aggregateProperty);
    return _.chain(group).first().pick(propertyNames).extend({
        [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
    }).value();
}

// Get an array with all the computed aggregates.
const result = _.map(intermediate, summarize);

给定之前获得的“ intermediate ”,并将“ aggregateProperty ”设置为“ Value ”,我们得到了问题提问者所需的“ result ”:
[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

我们可以将所有内容放在一个函数中,该函数以 arrayOfObjectspropertyNamesaggregateProperty 作为参数。请注意,arrayOfObjects 实际上也可以是具有字符串键的普通对象,因为 _.groupBy 接受任何一种类型。因此,我已将 arrayOfObjects 重命名为 collection
function aggregate(collection, propertyNames, aggregateProperty) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    const addNumeric = (a, b) => +a + +b;
    function summarize(group) {
        const valuesToSum = _.pluck(group, aggregateProperty);
        return _.chain(group).first().pick(propertyNames).extend({
            [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
        }).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

aggregate(arrayOfObjects, ['Phase', 'Step'], 'Value')现在会再次给我们相同的result

我们可以更进一步,使调用者能够计算每个组中值的任何统计信息。我们可以做到这一点,还可以使调用者向每个组的摘要添加任意属性。我们可以在代码更短的情况下完成所有这些操作。我们通过将aggregateProperty参数替换为iteratee参数并将其直接传递给_.reduce来实现这一点:

function aggregate(collection, propertyNames, iteratee) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    function summarize(group) {
        return _.chain(group).first().pick(propertyNames)
            .extend(_.reduce(group, iteratee)).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

实际上,我们将一些责任转移给调用者;她必须提供一个可以传递给_.reduceiteratee,以便调用_.reduce将生成一个带有她想要添加的聚合属性的对象。例如,我们使用以下表达式获得与之前相同的result
aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: +memo.Value + +value.Value
}));

举个稍微复杂一些的iteratee的例子,假设我们想计算每个组的最大Value而不是总和,并且我们想添加一个Tasks属性来列出组中出现的所有Task的值。下面是一种使用上面提到的aggregate的最后一个版本(以及_.union)的方法:

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: Math.max(memo.Value, value.Value),
    Tasks: _.union(memo.Tasks || [memo.Task], [value.Task])
}));

我们得到以下结果:
[
    { Phase: "Phase 1", Step: "Step 1", Value: 10, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 1", Step: "Step 2", Value: 20, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 1", Value: 30, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 2", Value: 40, Tasks: [ "Task 1", "Task 2" ] }
]

感谢@much2learn提供了一个答案,可以处理任意的归约函数。我写了更多的SO答案,演示了如何通过组合多个Underscore函数来实现复杂的操作:


3

Array.prototype.groupBy = function (groupingKeyFn) {
    if (typeof groupingKeyFn !== 'function') {
        throw new Error("groupBy take a function as only parameter");
    }
    return this.reduce((result, item) => {
        let key = groupingKeyFn(item);
        if (!result[key])
            result[key] = [];
        result[key].push(item);
        return result;
    }, {});
}

var a = [
 {type: "video", name: "a"},
  {type: "image", name: "b"},
  {type: "video", name: "c"},
  {type: "blog", name: "d"},
  {type: "video", name: "e"},
]
console.log(a.groupBy((item) => item.type));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


3
我会检查 declarative-js groupBy,它似乎正是你要找的。它还具有以下特点:
  • 非常高效(性能基准测试
  • 使用 TypeScript 编写,因此所有类型信息都已包含。
  • 不强制使用第三方类数组对象。
import { Reducers } from 'declarative-js';
import groupBy = Reducers.groupBy;
import Map = Reducers.Map;

const data = [
    { 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" }
];

data.reduce(groupBy(element=> element.Step), Map());
data.reduce(groupBy('Step'), Map());

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