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


506

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

例如,给定此对象数组:

[ 
    { 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方法。

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

因此,如果我进行了groupby 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,然后循环遍历生成的对象来自己做总计?

Answers:


755

如果要避免使用外部库,则可以简洁地实现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"]}


18
我会这样修改:```return xs.reduce(function(rv,x){var v = Function的key instance?key(x):x [key];(rv [v] = rv [v] || [])。push(x); return rv;},{}); ```允许回调函数返回排序标准
y_nk '16

109
这是一个输出数组而不是对象的数组:groupByArray(xs,key){return xs.reduce(function(rv,x){let v = Key instanceof Function?key(x):x [key]; let el = rv .find((r)=> r && r.key === v); if(el){el.values.push(x);} else {rv.push({key:v,values:[x] });} return rv;},[]); }
tomitrescak

24
太好了,正是我所需要的。如果有人需要它,这里是TypeScript签名:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Michael Sandino

4
就其价值而言,tomitrescak的解决方案虽然方便,但效率却大大降低,因为find()可能是O(n)。答案中的解为O(n),来自reduce(对象分配为O(1),推入为),而注释为O(n)* O(n)或O(n ^ 2)或at至少O(nlgn)
narthur157 '18

21
如果有人感兴趣,我对该函数进行了更具可读性和注释性的版本,并放入了要点:gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d我发现它有助于理解此处实际发生的情况。
robmathers 18-10-25

227

使用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"}]
    
    

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


@mortb,如何在不调用get()方法的情况下获取它?我想在不通过键的情况下显示输出
Fai Zal Dong

@FaiZalDong:我不确定哪种方法最适合您的情况?如果我console.log(grouped.entries());在jsfiddle示例中编写,它将返回一个可迭代的对象,其行为类似于键+值的数组。您可以尝试一下,看看是否有帮助?
mortb

7
您也可以尝试console.log(Array.from(grouped));
mortb

我喜欢这个答案,非常灵活
-benshabatnoam

查看组中元素的数量:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
AhmetŞimşek19年

105

使用ES6:

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

3
这需要一些习惯,但这样做的大部分的C ++模板以及
列维哈斯克尔

2
我绞尽脑汁,仍然无法理解世界从那里开始是如何工作的...result。因此,我无法入睡。

7
优雅,但在大型阵列上却很慢!
infinity1975

4
大声笑这是什么
NinoŠkopac

1
简单的解决方案。谢谢
Ezequiel Tavares

59

尽管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)});
});

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

has尽管我可能会遗漏它,但在Underscore中我看不到能做什么的任何东西。它与几乎相同_.contains,但是用于_.isEqual而不是===用于比较。除此之外,其余的都是特定于问题的,尽管尝试是通用的。

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

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

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}
]

但是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)});
});

现在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}
]

或者如果您注册了此:

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

然后打电话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)"}
]

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

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


嗨..可以看到您按一个值进行分组和求和,但是如果我想按value1和valu2和value3求和...您有解决方案吗?
SAMUEL OSPINA '16

@SAMUELOSPINA您是否找到一种方法来执行此操作?
howMuchCheeseIsTooMuchCheese

50

我会检查lodash groupBy,它看起来确实可以满足您的需求。它也很轻巧,非常简单。

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

假设您的数组名称为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

1
这个答案有问题吗?lodash _.groupBy结果有多种方式不符合OP请求的结果格式。(1)结果不是数组。(2)“值”已成为lodash对象结果中的“关键字”。
mg1075

44

使用可能更容易做到linq.js这一点,它旨在真正实现LINQ in JavaScript(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();

结果:

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

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

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

我们可以在此处分组时使用多个属性GroupBy(function(x){ return x.Phase; })
Amit 2015年

36

您可以Map从构建ES6 array.reduce()

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

与其他解决方案相比,它具有一些优点:

  • 它不需要任何库(例如_.groupBy()
  • 您得到的是JavaScript,Map而不是对象(例如,由返回_.groupBy())。这有很多好处,包括:
    • 它会记住最初添加项目的顺序,
    • 键可以是任何类型,而不仅仅是字符串。
  • A Map是比数组更有用的结果。但是,如果您确实想要一个数组数组,则可以调用Array.from(groupedMap.entries())(对于[key, group array]成对数组)或Array.from(groupedMap.values())(对于简单数组)。
  • 它非常灵活;通常,您计划对此地图进行下一步操作的任何操作都可以直接作为简化操作的一部分来完成。

作为最后一点的示例,假设我有一个对象数组,我想通过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"}]

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

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

1
我不知道为什么没有更多的选票。它的简洁,可读性(对我来说)和容貌高效。 它无法在IE11上运行,但改装的难度不大(a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map())大约)
取消


18

您可以使用Alasql JavaScript库执行此操作:

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]);

在jsFiddle尝试此示例。

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

注释:

  • 在这里,我将Value放在方括号中,因为VALUE是SQL中的关键字
  • 我必须使用CAST()函数将字符串值转换为数字类型。

18
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]
        };
    });
};

15

MDN 在其文档中包含此示例Array.reduce()

// Grouping objects by a property
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce#Grouping_objects_by_a_property#Grouping_objects_by_a_property

var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// { 
//   20: [
//     { name: 'Max', age: 20 }, 
//     { name: 'Jane', age: 20 }
//   ], 
//   21: [{ name: 'Alice', age: 21 }] 
// }

14

尽管问题有一些答案,而且答案看起来有些复杂,但我建议使用带有嵌套嵌套的香草Javascript(如有必要)Map

function groupBy(array, groups, valueKey) {
    var map = new Map;
    groups = [].concat(groups);
    return array.reduce((r, o) => {
        groups.reduce((m, k, i, { length }) => {
            var child;
            if (m.has(o[k])) return m.get(o[k]);
            if (i + 1 === length) {
                child = Object
                    .assign(...groups.map(k => ({ [k]: o[k] })), { [valueKey]: 0 });
                r.push(child);
            } else {
                child = new Map;
            }
            m.set(o[k], child);
            return child;
        }, map)[valueKey] += +o[valueKey];
        return r;
    }, [])
};

var 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" }];

console.log(groupBy(data, 'Phase', 'Value'));
console.log(groupBy(data, ['Phase', 'Step'], 'Value'));
.as-console-wrapper { max-height: 100% !important; top: 0; }


9

无突变:

const groupBy = (xs, key) => xs.reduce((acc, x) => Object.assign({}, acc, {
  [x[key]]: (acc[x[key]] || []).concat(x)
}), {})

console.log(groupBy(['one', 'two', 'three'], 'length'));
// => {3: ["one", "two"], 5: ["three"]}

8

该解决方案采用任意函数(不是键),因此比上述解决方案更加灵活,并允许使用箭头函数,这些箭头函数类似于LINQ中使用的lambda表达式

Array.prototype.groupBy = function (funcProp) {
    return this.reduce(function (acc, val) {
        (acc[funcProp(val)] = acc[funcProp(val)] || []).push(val);
        return acc;
    }, {});
};

注意:是否要扩展Array的原型由您决定。

大多数浏览器支持的示例:

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(function(c){return c.a;})

使用箭头功能(ES6)的示例:

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(c=>c.a)

上面的两个示例都返回:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}

我非常喜欢ES6解决方案。只是略微简化而未扩展Array原型:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
caneta

8

我想建议我的方法。首先,分别分组和汇总。让我们声明典型的“分组依据”功能。它需要另一个函数为每个要分组的数组元素生成“哈希”字符串。

Array.prototype.groupBy = function(hash){
  var _hash = hash ? hash : function(o){return o;};

  var _map = {};
  var put = function(map, key, value){
    if (!map[_hash(key)]) {
        map[_hash(key)] = {};
        map[_hash(key)].group = [];
        map[_hash(key)].key = key;

    }
    map[_hash(key)].group.push(value); 
  }

  this.map(function(obj){
    put(_map, obj, obj);
  });

  return Object.keys(_map).map(function(key){
    return {key: _map[key].key, group: _map[key].group};
  });
}

完成分组后,您可以根据需要汇总数据

data.groupBy(function(o){return JSON.stringify({a: o.Phase, b: o.Step});})
    /* aggreagating */
    .map(function(el){ 
         var sum = el.group.reduce(
           function(l,c){
             return l + parseInt(c.Value);
           },
           0
         );
         el.key.Value = sum; 
         return el.key;
    });

共同点是可行的。我已经在Chrome控制台中测试了此代码。并随时改善并发现错误;)


谢谢 !喜欢这种方法,并且非常适合我的需求(我不需要汇总)。
aberaud 2014年

我认为您想将put():中的行更改map[_hash(key)].key = key;map[_hash(key)].key = _hash(key);
Scotty.NET 2015年

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仅允许唯一值,因此将删除所有重复项。

  1. 现在重复项不见了,我们将使用散布运算符将其转换回数组。

设置文档:https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/设置传播操作员文档:https : //developer.mozilla.org/en-US/docs/Web/JavaScript /参考/运算符/ Spread_syntax


我非常喜欢您的答案,这是最短的答案,但是我仍然不明白其逻辑,尤其是,谁在这里分组?它是传播算子(...)吗?还是'new Set()'?请向我们解释一下...谢谢
Ivan

1
1.首先,我们通过传递一个数组来创建一个新的Set。由于Set仅允许唯一值,因此将删除所有重复项。2.现在重复项已经不存在了,我们将使用传播运算符将其转换回数组。设置文档:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…传播运营商:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
亚戈Gehres

好的,知道了...感谢您的解释先生:)
伊万

别客气!
Yago Gehres

6

选中的答案-只是浅层分组。了解减少非常好。问题还提供了其他总计计算的问题。

这是一个按对象字段排列对象数组的REAL GROUP BY,带有1)计算的键名和2)通过提供所需键的列表并将其唯一值转换为根键(例如SQL GROUP)来完成级联分组的完整解决方案BY。

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);

试玩estKey-您可以将一个以上的字段分组,添加其他汇总,计算或其他处理。

您也可以递归地对数据进行分组。例如,最初按分组Phase,然后按Step字段分组,依此类推。此外,删除多余的脂肪数据。

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" }
  ];

/**
 * Small helper to get SHALLOW copy of obj WITHOUT prop
 */
const rmProp = (obj, prop) => ( (({[prop]:_, ...rest})=>rest)(obj) )

/**
 * Group Array by key. Root keys of a resulting array is value
 * of specified key.
 *
 * @param      {Array}   src     The source array
 * @param      {String}  key     The by key to group by
 * @return     {Object}          Object with groupped objects as values
 */
const grpBy = (src, key) => src.reduce((a, e) => (
  (a[e[key]] = a[e[key]] || []).push(rmProp(e, key)),  a
), {});

/**
 * Collapse array of object if it consists of only object with single value.
 * Replace it by the rest value.
 */
const blowObj = obj => Array.isArray(obj) && obj.length === 1 && Object.values(obj[0]).length === 1 ? Object.values(obj[0])[0] : obj;

/**
 * Recoursive groupping with list of keys. `keyList` may be an array
 * of key names or comma separated list of key names whom UNIQUE values will
 * becomes the keys of the resulting object.
 */
const grpByReal = function (src, keyList) {
  const [key, ...rest] = Array.isArray(keyList) ? keyList : String(keyList).trim().split(/\s*,\s*/);
  const res = key ? grpBy(src, key) : [...src];
  if (rest.length) {
for (const k in res) {
  res[k] = grpByReal(res[k], rest)
}
  } else {
for (const k in res) {
  res[k] = blowObj(res[k])
}
  }
  return res;
}

console.log( JSON.stringify( grpByReal(inputArray, 'Phase, Step, Task'), null, 2 ) );


5
groupByArray(xs, key) {
    return xs.reduce(function (rv, x) {
        let v = key instanceof Function ? key(x) : x[key];
        let el = rv.find((r) => r && r.key === v);
        if (el) {
            el.values.push(x);
        }
        else {
            rv.push({
                key: v,
                values: [x]
            });
        }
        return rv;
    }, []);
}

这个输出数组。


4

根据先前的答案

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],
  }), {});

在这里,我们的化简器获取部分形成的返回值(从一个空对象开始),并返回一个对象,该对象由先前返回值的散布成员以及一个新成员组成,该新成员的密钥是根据当前受迭代者的值计算得出的prop并且其值是该道具的所有值以及当前值的列表。


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

这是一个使用ES6的令人讨厌,难以阅读的解决方案:

export default (arr, key) => 
  arr.reduce(
    (r, v, _, __, k = v[key]) => ((r[k] || (r[k] = [])).push(v), r),
    {}
  );

对于那些问这甚至如何工作的人,这里有一个解释:

  • 两者=>都有return

  • Array.prototype.reduce功能最多包含4个参数。这就是为什么要添加第五个参数的原因,因此我们可以使用默认值在参数声明级别为组(k)提供便宜的变量声明。(是的,这是巫术)

  • 如果当前组在上一次迭代中不存在,我们将创建一个新的空数组,((r[k] || (r[k] = []))该数组将求值为最左边的表达式,换句话说,是一个现有数组或一个空数组,这就是push在该表达式之后有一个立即数的原因,因为无论哪种方式,您都会得到一个数组。

  • 当存在时return,逗号,运算符将舍弃最左边的值,并返回针对此场景进行了调整的前一组。

易于理解的相同功能版本是:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});

2
您是否愿意解释一下,效果很好
Nuwan Dammika,

@NuwanDammika-在两个=>中,您都有一个免费的“返回”-reduce函数最多需要4个参数。这就是为什么要添加第五个参数,以便为组(k)提供便宜的变量声明的原因。-如果先前的值没有我们的当前组,我们将创建一个新的空组((r [k] ||(r [k] = [])))这将得出最左边的表达式,否则为数组或空数组,这就是为什么在该表达式后立即进行推送的原因-当有返回值时,逗号运算符将舍弃最左边的值,返回经过调整的前一组
darkndream

2

让我们生成一个通用Array.prototype.groupBy()工具。仅出于多样性考虑,让我们对递归方法使用ES6奇异的散布算子进行某些Haskellesque模式匹配。另外,让我们Array.prototype.groupBy()接受一个回调,该回调将item(e),index(i)和所应用的array(a)作为参数。

Array.prototype.groupBy = function(cb){
                            return function iterate([x,...xs], i = 0, r = [[],[]]){
                                     cb(x,i,[x,...xs]) ? (r[0].push(x), r)
                                                       : (r[1].push(x), r);
                                     return xs.length ? iterate(xs, ++i, r) : r;
                                   }(this);
                          };

var arr = [0,1,2,3,4,5,6,7,8,9],
    res = arr.groupBy(e => e < 5);
console.log(res);


2

Ceasar的回答很好,但是仅适用于数组内部元素的内部属性(对于字符串,为长度)。

此实现的工作方式类似于:此链接

const groupBy = function (arr, f) {
    return arr.reduce((out, val) => {
        let by = typeof f === 'function' ? '' + f(val) : val[f];
        (out[by] = out[by] || []).push(val);
        return out;
    }, {});
};

希望这可以帮助...


2

从@ mortb,@ jmarceli回答以及从这篇文章中

我利用的优势是分组依据JSON.stringify()原始值的多个列的标识。

没有第三方

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

const pets = [
    {type:"Dog", age: 3, name:"Spot"},
    {type:"Cat", age: 3, name:"Tiger"},
    {type:"Dog", age: 4, name:"Rover"}, 
    {type:"Cat", age: 3, name:"Leo"}
];

const grouped = groupBy(pets,
pet => JSON.stringify({ type: pet.type, age: pet.age }));

console.log(grouped);

Lodash第三方

const pets = [
    {type:"Dog", age: 3, name:"Spot"},
    {type:"Cat", age: 3, name:"Tiger"},
    {type:"Dog", age: 4, name:"Rover"}, 
    {type:"Cat", age: 3, name:"Leo"}
];

let rslt = _.groupBy(pets, pet => JSON.stringify(
 { type: pet.type, age: pet.age }));

console.log(rslt);

keyGetter返回未定义
Asbar Ali,

@AsbarAli我使用Chrome的控制台-版本66.0.3359.139(正式版本)(64位)测试了我的代码段。一切运行正常。您能否放置调试断点并查看为什么未定义keyGetter。可能是因为浏览器版本。
Pranithan T.

2

具有 reduce基于ES6版本的功能的支持iteratee

如果iteratee未提供功能,则按预期方式工作:

const data = [{id: 1, score: 2},{id: 1, score: 3},{id: 2, score: 2},{id: 2, score: 4}]

const group = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});

const groupBy = (arr, k, fn = () => true) => 
  arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});

console.log(group(data, 'id'))     // grouping via `reduce`
console.log(groupBy(data, 'id'))   // same result if `fn` is omitted
console.log(groupBy(data, 'score', x => x > 2 )) // group with the iteratee

在OP问题中:

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

const groupBy = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});
const groupWith = (arr, k, fn = () => true) => 
  arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});

console.log(groupBy(data, 'Phase'))
console.log(groupWith(data, 'Value', x => x > 30 ))  // group by `Value` > 30

另一个ES6版本,它反转了分组并使用valuesas keyskeysas作为grouped values

const data = [{A: "1"}, {B: "10"}, {C: "10"}]

const groupKeys = arr => 
  arr.reduce((r,c) => (Object.keys(c).map(x => r[c[x]] = [...r[c[x]] || [], x]),r),{});

console.log(groupKeys(data))

注意:为了简洁起见,功能只是以其简短形式(一行)发布,并且仅与概念相关。您可以展开它们并添加其他错误检查等。


2

我将检查declarative-js, groupBy它似乎完全符合您的要求。也是:

  • 表现出色(性能基准
  • 用打字稿写的,所以包括所有打字。
  • 它不是强制使用类似第三方数组的对象。
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());

1
let groupbyKeys = function(arr, ...keys) {
  let keysFieldName = keys.join();
  return arr.map(ele => {
    let keysField = {};
    keysField[keysFieldName] = keys.reduce((keyValue, key) => {
      return keyValue + ele[key]
    }, "");
    return Object.assign({}, ele, keysField);
  }).reduce((groups, ele) => {
    (groups[ele[keysFieldName]] = groups[ele[keysFieldName]] || [])
      .push([ele].map(e => {
        if (keys.length > 1) {
          delete e[keysFieldName];
        }
        return e;
    })[0]);
    return groups;
  }, {});
};

console.log(groupbyKeys(array, 'Phase'));
console.log(groupbyKeys(array, 'Phase', 'Step'));
console.log(groupbyKeys(array, 'Phase', 'Step', 'Task'));

1

这是一个不会破坏null成员的ES6版本

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

1

只是为了增加Scott Sauyet的答案,有人在评论中询问如何使用其功能对value1,value2等进行分组,而不是仅对一个值进行分组。

只需编辑他的sum函数即可:

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

保留主要的(DataGrouper)不变:

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;
}());

1

具有分类功能

export const groupBy = function groupByArray(xs, key, sortKey) {
      return xs.reduce(function(rv, x) {
        let v = key instanceof Function ? key(x) : x[key];
        let el = rv.find(r => r && r.key === v);

        if (el) {
          el.values.push(x);
          el.values.sort(function(a, b) {
            return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());
          });
        } else {
          rv.push({ key: v, values: [x] });
        }

        return rv;
      }, []);
    };

样品:

var state = [
    {
      name: "Arkansas",
      population: "2.978M",
      flag:
  "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg",
      category: "city"
    },{
      name: "Crkansas",
      population: "2.978M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg",
      category: "city"
    },
    {
      name: "Balifornia",
      population: "39.14M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/0/01/Flag_of_California.svg",
      category: "city"
    },
    {
      name: "Florida",
      population: "20.27M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Florida.svg",
      category: "airport"
    },
    {
      name: "Texas",
      population: "27.47M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Texas.svg",
      category: "landmark"
    }
  ];
console.log(JSON.stringify(groupBy(state,'category','name')));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.