Javascript 对对象数组进行分组的最有效方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14446511/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 16:47:58  来源:igfitidea点击:

Most efficient method to groupby on an array of objects

javascriptarraysobjectgroup-byunderscore.js

提问by Rail24

What is the most efficient way to groupby objects in an array?

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

For example, given this array of objects:

例如,给定这个对象数组:

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

I'm displaying this information in a table. I'd like to groupby different methods, but I want to sum the values.

我在表格中显示此信息。我想对不同的方法进行分组,但我想对这些值求和。

I'm using Underscore.js for its groupby function, which is helpful, but doesn't do the whole trick, because I don't want them “split up” but “merged”, more like the SQL group bymethod.

我将 Underscore.js 用于它的 groupby 函数,这很有帮助,但并不能解决所有问题,因为我不希望它们“拆分”而是“合并”,更像是 SQLgroup by方法。

What I'm looking for would be able to total specific values (if requested).

我正在寻找的是能够总计特定值(如果需要)。

So if I did groupby Phase, I'd want to receive:

因此,如果我执行 groupby Phase,我希望收到:

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

And if I did groupy Phase/ Step, I'd receive:

如果我做了 groupy 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 }
]

Is there a helpful script for this, or should I stick to using Underscore.js, and then looping through the resulting object to do the totals myself?

是否有一个有用的脚本,或者我应该坚持使用 Underscore.js,然后循环遍历结果对象来自己计算总数?

回答by Ceasar Bautista

If you want to avoid external libraries, you can concisely implement a vanilla version of groupBy()like so:

如果你想避免使用外部库,你可以简洁地实现一个groupBy()像这样的香草版本:

var groupBy = function(xs, key) {
  return xs.reduce(function(rv, x) {
    (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
  }, {});
};

console.log(groupBy(['one', 'two', 'three'], 'length'));

// => {3: ["one", "two"], 5: ["three"]}

回答by mortb

Using ES6 Map object:

使用 ES6 Map 对象:

function groupBy(list, keyGetter) {
    const map = new Map();
    list.forEach((item) => {
         const key = keyGetter(item);
         const collection = map.get(key);
         if (!collection) {
             map.set(key, [item]);
         } else {
             collection.push(item);
         }
    });
    return map;
}

// example usage

const pets = [
    {type:"Dog", name:"Spot"},
    {type:"Cat", name:"Tiger"},
    {type:"Dog", name:"Rover"}, 
    {type:"Cat", name:"Leo"}
];
    
const grouped = groupBy(pets, pet => pet.type);
    
console.log(grouped.get("Dog")); // -> [{type:"Dog", name:"Spot"}, {type:"Dog", name:"Rover"}]
console.log(grouped.get("Cat")); // -> [{type:"Cat", name:"Tiger"}, {type:"Cat", name:"Leo"}]
    
    

About Map: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

关于地图:https: //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

回答by Joseph Nields

with ES6:

使用 ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);

回答by Scott Sauyet

Although the linqanswer is interesting, it's also quite heavy-weight. My approach is somewhat different:

尽管linq 的答案很有趣,但它也很重要。我的方法有些不同:

var DataGrouper = (function() {
    var has = function(obj, target) {
        return _.any(obj, function(value) {
            return _.isEqual(value, target);
        });
    };

    var keys = function(data, names) {
        return _.reduce(data, function(memo, item) {
            var key = _.pick(item, names);
            if (!has(memo, key)) {
                memo.push(key);
            }
            return memo;
        }, []);
    };

    var group = function(data, names) {
        var stems = keys(data, names);
        return _.map(stems, function(stem) {
            return {
                key: stem,
                vals:_.map(_.where(data, stem), function(item) {
                    return _.omit(item, names);
                })
            };
        });
    };

    group.register = function(name, converter) {
        return group[name] = function(data, names) {
            return _.map(group(data, names), converter);
        };
    };

    return group;
}());

DataGrouper.register("sum", function(item) {
    return _.extend({}, item.key, {Value: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.Value);
    }, 0)});
});

You can see it in action on JSBin.

您可以在 JSBin 上看到它的运行情况

I didn't see anything in Underscore that does what hasdoes, although I might be missing it. It's much the same as _.contains, but uses _.isEqualrather than ===for comparisons. Other than that, the rest of this is problem-specific, although with an attempt to be generic.

我在 Underscore 中没有看到任何可以做的事情has,尽管我可能会错过它。它与 非常相似_.contains,但用于_.isEqual而不是===用于比较。除此之外,其余的都是特定于问题的,尽管尝试通用。

Now DataGrouper.sum(data, ["Phase"])returns

现在DataGrouper.sum(data, ["Phase"])返回

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

And DataGrouper.sum(data, ["Phase", "Step"])returns

DataGrouper.sum(data, ["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}
]


But sumis only one potential function here. You can register others as you like:

但这sum只是这里的一种潜在功能。您可以根据需要注册其他人:

DataGrouper.register("max", function(item) {
    return _.extend({}, item.key, {Max: _.reduce(item.vals, function(memo, node) {
        return Math.max(memo, Number(node.Value));
    }, Number.NEGATIVE_INFINITY)});
});

and now DataGrouper.max(data, ["Phase", "Step"])will return

现在DataGrouper.max(data, ["Phase", "Step"])会回来

[
    {Phase: "Phase 1", Step: "Step 1", Max: 10},
    {Phase: "Phase 1", Step: "Step 2", Max: 20},
    {Phase: "Phase 2", Step: "Step 1", Max: 30},
    {Phase: "Phase 2", Step: "Step 2", Max: 40}
]

or if you registered this:

或者如果你注册了这个:

DataGrouper.register("tasks", function(item) {
    return _.extend({}, item.key, {Tasks: _.map(item.vals, function(item) {
      return item.Task + " (" + item.Value + ")";
    }).join(", ")});
});

then calling DataGrouper.tasks(data, ["Phase", "Step"])will get you

然后打电话DataGrouper.tasks(data, ["Phase", "Step"])会让你

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

DataGrouperitself is a function. You can call it with your data and a list of the properties you want to group by. It returns an array whose elements are object with two properties: keyis the collection of grouped properties, valsis an array of objects containing the remaining properties not in the key. For example, DataGrouper(data, ["Phase", "Step"])will yield:

DataGrouper本身就是一个函数。您可以使用您的数据和要分组的属性列表调用它。它返回一个数组,其元素是具有两个属性的对象:key是分组属性的集合,vals是包含不在键中的其余属性的对象数组。例如,DataGrouper(data, ["Phase", "Step"])将产生:

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

DataGrouper.registeraccepts a function and creates a new function which accepts the initial data and the properties to group by. This new function then takes the output format as above and runs your function against each of them in turn, returning a new array. The function that's generated is stored as a property of DataGrouperaccording to a name you supply and also returned if you just want a local reference.

DataGrouper.register接受一个函数并创建一个新函数,该函数接受初始数据和要分组的属性。然后,这个新函数采用上述输出格式,并依次针对它们中的每一个运行您的函数,返回一个新数组。生成的函数DataGrouper根据您提供的名称存储为属性,如果您只需要本地引用,也会返回。

Well that's a lot of explanation. The code is reasonably straightforward, I hope!

嗯,这是很多解释。代码相当简单,我希望!

回答by jmarceli

I would check lodash groupByit seems to do exactly what you are looking for. It is also quite lightweight and really simple.

我会检查lodash groupBy它似乎完全符合您的要求。它也很轻巧,非常简单。

Fiddle example: https://jsfiddle.net/r7szvt5k/

小提琴示例:https: //jsfiddle.net/r7szvt5k/

Provided that your array name is arrthe groupBy with lodash is just:

假设您的数组名称是arr带有 lodash 的 groupBy 只是:

import groupBy from 'lodash/groupBy';
// if you still use require:
// const groupBy = require('lodash/groupBy');

const a = groupBy(arr, function(n) {
  return n.Phase;
});
// a is your array grouped by Phase attribute

回答by mellamokb

This is probably more easily done with linq.js, which is intended to be a true implementation of LINQ in JavaScript (DEMO):

这可能更容易完成linq.js,它旨在成为 JavaScript 中 LINQ 的真正实现(DEMO):

var linq = Enumerable.From(data);
var result =
    linq.GroupBy(function(x){ return x.Phase; })
        .Select(function(x){
          return {
            Phase: x.Key(),
            Value: x.Sum(function(y){ return y.Value|0; })
          };
        }).ToArray();

result:

结果:

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

Or, more simply using the string-based selectors (DEMO):

或者,更简单地使用基于字符串的选择器 ( DEMO):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();

回答by Arthur Tacca

You can build an ES6 Mapfrom array.reduce().

您可以Maparray.reduce().

const groupedMap = initialArray.reduce(
    (entryMap, e) => entryMap.set(e.id, [...entryMap.get(e.id)||[], e]),
    new Map()
);

This has a few advantages over the other solutions:

与其他解决方案相比,这有一些优势:

  • It doesn't require any libraries (unlike e.g. _.groupBy())
  • You get a JavaScript Maprather than an object (e.g. as returned by _.groupBy()). This has lots of benefits, including:
    • it remembers the order in which items were first added,
    • keys can be any type rather than just strings.
  • A Mapis a more useful result that an array of arrays. But if you do want an array of arrays, you can then call Array.from(groupedMap.entries())(for an array of [key, group array]pairs) or Array.from(groupedMap.values())(for a simple array of arrays).
  • It's quite flexible; often, whatever you were planning to do next with this map can be done directly as part of the reduction.
  • 它不需要任何库(与 eg 不同_.groupBy()
  • 您得到的是 JavaScriptMap而不是对象(例如,由 返回_.groupBy())。这有很多好处,包括:
    • 它会记住第一次添加项目的顺序,
    • 键可以是任何类型而不仅仅是字符串。
  • AMap是比数组数组更有用的结果。但是如果你确实想要一个数组数组,那么你可以调用Array.from(groupedMap.entries())(对于一个数组[key, group array]对)或Array.from(groupedMap.values())(对于一个简单的数组数组)。
  • 它非常灵活;通常,您接下来打算使用此地图执行的任何操作都可以作为缩减的一部分直接完成。

As an example of the last point, imagine I have an array of objects that I want to do a (shallow) merge on by id, like this:

作为最后一点的一个例子,假设我有一个对象数组,我想按 id 对其进行(浅)合并,如下所示:

const objsToMerge = [{id: 1, name: "Steve"}, {id: 2, name: "Alice"}, {id: 1, age: 20}];
// The following variable should be created automatically
const mergedArray = [{id: 1, name: "Steve", age: 20}, {id: 2, name: "Alice"}]

To do this, I would usually start by grouping by id, and then merging each of the resulting arrays. Instead, you can do the merge directly in the reduce():

为此,我通常首先按 id 分组,然后合并每个结果数组。相反,您可以直接在以下文件中进行合并reduce()

const mergedArray = Array.from(
    objsToMerge.reduce(
        (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}),
        new Map()
    ).values()
);

回答by Julio Marins

_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

From: http://underscorejs.org/#groupBy

来自:http: //underscorejs.org/#groupBy

回答by agershun

You can do it with AlasqlJavaScript library:

您可以使用AlasqlJavaScript 库来实现:

var data = [ { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
             { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" }];

var res = alasql('SELECT Phase, Step, SUM(CAST([Value] AS INT)) AS [Value] \
                  FROM ? GROUP BY Phase, Step',[data]);

Try this example at jsFiddle.

在 jsFiddle试试这个例子。

BTW:On large arrays (100000 records and more) Alasql faster tham Linq. See test at jsPref.

顺便说一句:在大型阵列(100000 条记录和更多)上,Alasql 比 Linq 更快。请参阅jsPref 上的测试。

Comments:

注释:

  • Here I put Value in square brackets, because VALUE is a keyword in SQL
  • I have to use CAST() function to convert string Values to number type.
  • 这里我把Value放在方括号里,因为VALUE是SQL中的关键字
  • 我必须使用 CAST() 函数将字符串值转换为数字类型。

回答by cezarypiatek

Array.prototype.groupBy = function(keyFunction) {
    var groups = {};
    this.forEach(function(el) {
        var key = keyFunction(el);
        if (key in groups == false) {
            groups[key] = [];
        }
        groups[key].push(el);
    });
    return Object.keys(groups).map(function(key) {
        return {
            key: key,
            values: groups[key]
        };
    });
};