关于javascript:对一组对象进行分组的最有效方法

Most efficient method to groupby on an array of objects

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

例如,给定此对象数组:

1
2
3
4
5
6
7
8
9
10
[
    { 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,我想要收到:

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

如果我做了分组Phase / Step,我会收到:

1
2
3
4
5
6
[
    { 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()的vanilla版本,如下所示:

1
2
3
4
5
6
7
8
9
10
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"]}


使用ES6 Map对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map


与ES6:

1
2
3
4
5
6
7
8
9
10
const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }),
  {},
);


虽然linq的答案很有趣,但它的重量也很重。我的方法有些不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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上看到它。

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

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

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

并且DataGrouper.sum(data, ["Phase","Step"])返回

1
2
3
4
5
6
[
    {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只是这里的一个潜在功能。您可以随意注册其他人:

1
2
3
4
5
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"])将返回

1
2
3
4
5
6
[
    {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}
]

或者如果你注册了这个:

1
2
3
4
5
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"])会得到你

1
2
3
4
5
6
[
    {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"])将产生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[
    {
       "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的属性,如果您只想要本地引用,也会返回该函数。

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


使用linq.js可能更容易做到这一点,linq.js旨在成为JavaScript(DEMO)中LINQ的真正实现:

1
2
3
4
5
6
7
8
9
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();

结果:

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

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

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


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

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

如果你的数组名称是arr,那么带有lodash的groupBy就是:

1
2
3
4
5
6
7
8
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
2
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

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


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

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

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

  • 它不需要任何库(不像例如_.groupBy())
  • 你得到一个JavaScript Map而不是一个对象(例如由_.groupBy()返回)。这有很多好处,包括:

    • 它会记住首次添加项目的顺序,
    • 键可以是任何类型而不仅仅是字符串。
  • Map是一个数组数组更有用的结果。但是如果你想要一个数组数组,那么你可以调用Array.from(groupedMap.entries())(对于一个[key, group array]对的数组)或Array.from(groupedMap.values())(对于一个简单的数组数组)。
  • 它非常灵活;通常,无论您计划在下一次使用此地图做什么,都可以直接作为缩减的一部分来完成。

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

1
2
3
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()中进行合并:

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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]
        };
    });
};


您可以使用Alasql JavaScript库来完成:

1
2
3
4
5
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()函数将字符串值转换为数字类型。

MDN在其Array.reduce()文档中有此示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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 }]
// }

虽然问题有一些答案,答案看起来有点过于复杂,但我建议使用vanilla Javascript进行分组(如果需要)Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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'));
1
.as-console-wrapper { max-height: 100% !important; top: 0; }


我想建议我的方法。首先,单独分组和聚合。让我们宣布典型的"分组依据"功能。它需要另一个函数来为每个要分组的数组元素生成"哈希"字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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};
  });
}

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

1
2
3
4
5
6
7
8
9
10
11
12
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控制台中测试了这段代码。随时改进并发现错误;)


没有突变:

1
2
3
4
5
6
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"]}


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

1
2
3
4
5
6
Array.prototype.groupBy = function (funcProp) {
    return this.reduce(function (acc, val) {
        (acc[funcProp(val)] = acc[funcProp(val)] || []).push(val);
        return acc;
    }, {});
};

注意:是否要扩展Array的原型取决于您。

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

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

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

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

以上两个例子都返回:

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


根据以前的答案

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

如果您的环境支持,使用对象扩展语法来查看它会更好一些。

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

这里,我们的reducer采用部分形成的返回值(从空对象开始),并返回一个由前一个返回值的展开成员组成的对象,以及一个新成员,其成员的密钥是根据当前迭代的值来计算的。 prop,其值是该道具的所有值以及当前值的列表。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
    }, []);
}

这个输出数组。


让我们生成一个通用的Array.prototype.groupBy()工具。只是为了变化,让我们使用ES6 fanciness扩展运算符来进行递归方法的一些Haskellesque模式匹配。另外,让我们的Array.prototype.groupBy()接受一个回调,它将项目(e)的索引(i)和应用的数组(a)作为参数。

1
2
3
4
5
6
7
8
9
10
11
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);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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));
1
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">


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

这个实现更像是这个链接

1
2
3
4
5
6
7
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;
    }, {});
};

希望这可以帮助...


基于ES6 reduce的版本版本,支持函数iteratee

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

1
2
3
4
5
6
7
8
9
10
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问题的背景下:

1
2
3
4
5
6
7
8
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版本,使用values作为keyskeys作为grouped values

1
2
3
4
5
6
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))

注意:为简洁起见,函数以简短形式(一行)发布,并仅涉及该想法。您可以展开它们并添加其他错误检查等。


只是为了补充Scott Sauyet的回答,有些人在评论中询问如何使用他的函数来组合value1,value2等,而不是仅仅分组一个值。

只需要编辑他的求和函数:

1
2
3
4
5
6
7
8
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)不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
      }, []);
    };

样品:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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')));


来自@mortb,@ jmarceli的回答和这篇文章,

我利用JSON.stringify()的优势成为PRIMITIVE VALUE的多列group by的标识。

没有第三方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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第三方合作

1
2
3
4
5
6
7
8
9
10
11
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);


这是一个ES6版本,不会在null成员上中断

1
2
3
4
5
6
function groupBy (arr, key) {
  return (arr || []).reduce((acc, x = {}) => ({
    ...acc,
    [x[key]]: [...acc[x[key]] || [], x]
  }), {})
}

检查答案 - 不是分组解决,但是直接的答案。

对于具有计算的键名称的某个字段的对象数组的REAL GROUP BY。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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字段分组。

自己验证,运行它。 Этотосамоеоно,чтолюдиназываютгруппировкой?

祝你成功。

Даздравствуютвысокиепоказателимастерствапрограммистоввоимяпроцветаниявсегочеловечества! Ура,товарищи!


我已经扩展了接受的答案,包括按多个属性分组,添加thenby并使其纯粹功能,没有变异。请访问https://stackblitz.com/edit/typescript-ezydzv查看演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export interface Group {
  key: any;
  items: any[];
}

export interface GroupBy {
  keys: string[];
  thenby?: GroupBy;
}

export const groupBy = (array: any[], grouping: GroupBy): Group[] => {
  const keys = grouping.keys;
  const groups = array.reduce((groups, item) => {
    const group = groups.find(g => keys.every(key => item[key] === g.key[key]));
    const data = Object.getOwnPropertyNames(item)
      .filter(prop => !keys.find(key => key === prop))
      .reduce((o, key) => ({ ...o, [key]: item[key] }), {});
    return group
      ? groups.map(g => (g === group ? { ...g, items: [...g.items, data] } : g))
      : [
          ...groups,
          {
            key: keys.reduce((o, key) => ({ ...o, [key]: item[key] }), {}),
            items: [data]
          }
        ];
  }, []);
  return grouping.thenby ? groups.map(g => ({ ...g, items: groupBy(g.items, grouping.thenby) })) : groups;
};

通常我使用Lodash JavaScript实用程序库和预先构建的groupBy()方法。它非常易于使用,请点击此处查看更多详细信息。


下面的函数允许任意字段的groupBy(和sum值 - OP需要什么)。在解决方案中,我们定义cmp函数以根据分组的fields比较两个对象。在let w=...中,我们创建子集对象x字段的副本。在y[sumBy]=+y[sumBy]+(+x[sumBy])中,我们使用'+'将字符串转换为数字。

1
2
3
4
5
6
7
8
9
function groupBy(data, fields, sumBy='Value') {
  let r=[], cmp= (x,y) => fields.reduce((a,b)=> a && x[b]==y[b], true);
  data.forEach(x=> {
    let y=r.find(z=>cmp(x,z));
    let w= [...fields,sumBy].reduce((a,b) => (a[b]=x[b],a), {})
    y ? y[sumBy]=+y[sumBy]+(+x[sumBy]) : r.push(w);
  });
  return r;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const d = [
    { 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" }
];



function groupBy(data, fields, sumBy='Value') {
  let r=[], cmp= (x,y) => fields.reduce((a,b)=> a && x[b]==y[b], true);
  data.forEach(x=> {
    let y=r.find(z=>cmp(x,z));
    let w= [...fields,sumBy].reduce((a,b) => (a[b]=x[b],a), {})
    y ? y[sumBy]=+y[sumBy]+(+x[sumBy]) : r.push(w);
  });
  return r;
}


// TEST
let p=(t,o) => console.log(t, JSON.stringify(o));
console.log('GROUP BY:');

p('Phase', groupBy(d,['Phase']) );
p('Step', groupBy(d,['Step']) );
p('Phase-Step', groupBy(d,['Phase', 'Step']) );
p('Phase-Task', groupBy(d,['Phase', 'Task']) );
p('Step-Task', groupBy(d,['Step', 'Task']) );
p('Phase-Step-Task', groupBy(d,['Phase','Step', 'Task']) );


我从underscore.js fiddler那里借用了这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
window.helpers=(function (){
    var lookupIterator = function(value) {
        if (value == null){
            return function(value) {
                return value;
            };
        }
        if (typeof value === 'function'){
                return value;
        }
        return function(obj) {
            return obj[value];
        };
    },
    each = function(obj, iterator, context) {
        var breaker = {};
        if (obj == null) return obj;
        if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
            obj.forEach(iterator, context);
        } else if (obj.length === +obj.length) {
            for (var i = 0, length = obj.length; i < length; i++) {
                if (iterator.call(context, obj[i], i, obj) === breaker) return;
            }
        } else {
            var keys = []
            for (var key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) keys.push(key)
            for (var i = 0, length = keys.length; i < length; i++) {
                if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
            }
        }
        return obj;
    },
    // An internal function used for aggregate"group by" operations.
    group = function(behavior) {
        return function(obj, iterator, context) {
            var result = {};
            iterator = lookupIterator(iterator);
            each(obj, function(value, index) {
                var key = iterator.call(context, value, index, obj);
                behavior(result, key, value);
            });
            return result;
        };
    };

    return {
      groupBy : group(function(result, key, value) {
        Object.prototype.hasOwnProperty.call(result, key) ? result[key].push(value) :              result[key] = [value];
        })
    };
})();

var arr=[{a:1,b:2},{a:1,b:3},{a:1,b:1},{a:1,b:2},{a:1,b:3}];
 console.dir(helpers.groupBy(arr,"b"));
 console.dir(helpers.groupBy(arr,function (el){
   return el.b>2;
 }));

您可以在数组上使用forEach并构造一组新项目。以下是使用FlowType注释执行此操作的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// @flow

export class Group< T > {
  tag: number
  items: Array< T >

  constructor() {
    this.items = []
  }
}

const groupBy = (items: Array< T >, map: (T) => number) => {
  const groups = []

  let currentGroup = null

  items.forEach((item) => {
    const tag = map(item)

    if (currentGroup && currentGroup.tag === tag) {
      currentGroup.items.push(item)
    } else {
      const group = new Group< T >()
      group.tag = tag
      group.items.push(item)
      groups.push(group)

      currentGroup = group
    }
  })

  return groups
}

export default groupBy

开玩笑测试可能就像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// @flow

import groupBy from './groupBy'

test('groupBy', () => {
  const items = [
    { name: 'January', month: 0 },
    { name: 'February', month: 1 },
    { name: 'February 2', month: 1 }
  ]

  const groups = groupBy(items, (item) => {
    return item.month
  })

  expect(groups.length).toBe(2)
  expect(groups[1].items[1].name).toBe('February 2')
})

1
2
3
4
5
6
data = [{id:1, name:'BMW'}, {id:2, name:'AN'}, {id:3, name:'BMW'}, {id:1, name:'NNN'}]
key = 'id'//try by id or name
data.reduce((previous, current)=>{
    previous[current[key]] && previous[current[key]].length != 0 ? previous[current[key]].push(current) : previous[current[key]] = new Array(current)
    return previous;
}, {})

你可以通过以下方式完成。我刚刚形成了新的数组并从groupBy函数返回它。通过.map函数循环计算的计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
var 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" }
    ];
var groupBy = (arr, pahse, step='') => {

   var pahseArr = [];
   var resultArr = [];

   arr.map((item)=>{
     var pushed = false;
     pahseArr.map((ele)=>{
       if(ele===item.Phase){
         pushed = true;
       }
     })
     if(!pushed){
       pahseArr.push(item.Phase);
     }    
   })

   pahseArr.map((item)=>{
      var sum = 0;
      arr.map((ele)=>{
        if(ele.Phase===item){
          sum += parseFloat(ele.Value)
        }
      })
      resultArr.push({
        Phase: item,
        Value: sum
      })
   })

   if(step!=''){
     var resultArr = [];


     pahseArr.map((item)=>{
         var stepArr = [];

         arr.map((item2)=>{
           var pushed = false;
           stepArr.map((ele)=>{
             if(ele===item2.Step){
               pushed = true;
             }
           })
           if(!pushed){
             stepArr.push(item2.Step);
           }
         })

         stepArr.map((item1)=>{
            var sum = 0;
            arr.map((ele)=>{
              if(ele.Step===item1 && ele.Phase===item){
                sum += parseFloat(ele.Value)
              }
            })
            resultArr.push({
              Phase: item,
              Step: item1,
              Value: sum
            })
         })

     })
     return resultArr;
   }  
   return resultArr;

}

console.log(groupBy(arr, 'Phase'));
console.log(groupBy(arr, 'Phase', 'Step'));


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

1
2
3
4
5
6
export default (array, key) => {
  return array.reduce(
    (r, v, _, __, k = v[key]) => ((r[k] || (r[k] = [])).push(v), r),
    {}
  );
};

基于@Ceasar Bautista的最初想法,我修改了代码并使用typescript创建了一个groupBy函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static groupBy(data: any[], comparator: (v1: any, v2: any) => boolean, onDublicate: (uniqueRow: any, dublicateRow: any) => void) {
    return data.reduce(function (reducedRows, currentlyReducedRow) {
      let processedRow = reducedRows.find(searchedRow => comparator(searchedRow, currentlyReducedRow));

      if (processedRow) {
        // currentlyReducedRow is a dublicateRow when processedRow is not null.
        onDublicate(processedRow, currentlyReducedRow)
      } else {
        // currentlyReducedRow is unique and must be pushed in the reducedRows collection.
        reducedRows.push(currentlyReducedRow);
      }

      return reducedRows;
    }, []);
  };

此函数接受一个回调(比较器),它比较行并找到dublicates和第二个回调聚合共享者的回调(onDublicate)。

用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data = [
    { name: 'a', value: 10 },
    { name: 'a', value: 11 },
    { name: 'a', value: 12 },
    { name: 'b', value: 20 },
    { name: 'b', value: 1 }
  ]

  private static demoComparator = (v1: any, v2: any) => {
    return v1['name'] === v2['name'];
  }

  private static demoOnDublicate = (uniqueRow, dublicateRow) => {
    uniqueRow['value'] += dublicateRow['value'];    
  };

调用

1
groupBy(data, demoComparator, demoOnDublicate)

将执行一个计算值之和的组。

1
2
{name:"a", value: 33}
{name:"b", value: 21}

我们可以根据项目的需要创建尽可能多的回调函数,并根据需要聚合值。例如,在一种情况下,我需要合并两个数组而不是对数据求和。