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

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

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

[{ 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函数,这很有帮助,但并不完整,因为我不希望它们“分裂”而是“合并”,更像SQLgroup by方法。

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

所以如果我做了GroupbyPhase,我想收到:

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

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

1061927 次浏览

使用#0可能更容易完成,它旨在成为JavaScript中LINQ的真正实现(演示):

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

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

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

虽然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所做的事情,尽管我可能会错过它。它与_.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的属性,如果你只是想要本地引用,也会返回。

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

我想建议我的方法。首先,分离分组和聚合。让我们声明原型的“group by”函数。它需要另一个函数为每个要分组的数组元素生成“哈希”字符串。

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控制台中测试了这段代码。并随时改进和发现错误;)

我从underscore.js小提琴手借用了这个方法

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

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

您可以使用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

BTW:在大型数组(100000条记录及更多)上,Alasql比Linq更快。参见测试在jsPref

评论:

  • 这里我把Value放在方括号中,因为VALUE是SQL中的关键字
  • 我必须使用CAST()函数将字符串值转换为数字类型。
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]};});};

我会选中泡菜组按,它似乎完全符合您的要求。它也非常轻量级且非常简单。

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

如果您的数组名称为arr,则带有洛达什的group pBy只是:

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

如果你想避免外部库,你可以简洁地实现groupBy()的vanilla版本,如下所示:

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

一种较新的方法,具有用于分组的对象和另外两个函数,用于创建一个键并获得一个具有所需分组项的对象和另一个用于添加值的键。

constgroupBy = (array, groups, valueKey) => {constgetKey = o => groups.map(k => o[k]).join('|'),getObject = o => Object.fromEntries([...groups.map(k => [k, o[k]]), [valueKey, 0]]);
groups = [].concat(groups);
return Object.values(array.reduce((r, o) => {(r[getKey(o)] ??= getObject(o))[valueKey] += +o[valueKey];return r;}, {}));},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; }

Old approach:

Although the question have some answers and the answers look a bit over complicated, I suggest to use vanilla Javascript for group-by with a nested (if necessary) 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; }

使用ES6 Map对象:

/*** @description* Takes an Array<V>, and a grouping function,* and returns a Map of the array grouped by the grouping function.** @param list An array of type V.* @param keyGetter A Function that takes the the Array type V as an input, and returns a value of type K.*                  K is generally intended to be a property key of V.** @returns Map of the array grouped by the grouping function.*///export function groupBy<K, V>(list: Array<V>, keyGetter: (input: V) => K): Map<K, Array<V>> {//    const map = new Map<K, Array<V>>();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"}]
const odd = Symbol();const even = Symbol();const numbers = [1,2,3,4,5,6,7];
const oddEven = groupBy(numbers, x => (x % 2 === 1 ? odd : even));    
console.log(oddEven.get(odd)); // -> [1,3,5,7]console.log(oddEven.get(even)); // -> [2,4,6]

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

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

这个输出数组。

无突变:

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表达式

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

让我们生成一个通用的Array.prototype.groupBy()工具。为了多样化,让我们使用ES6 fancness传播运算符在递归方法上进行一些Haskellesque模式匹配。此外,让我们的Array.prototype.groupBy()接受一个回调,该回调将项目(e)索引(i)和应用的数组(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);

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

希望这有助于…

使用ES6:

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

这是一个不会在空成员上崩溃的ES6版本

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

您可以从array.reduce()构建ES6Map

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

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

  • 它不需要任何库(不像_.groupBy()
  • 你会得到一个JavaScriptMap而不是一个对象(例如_.groupBy()返回的)。这有很多好处,包括:
    • 它会记住第一次添加项目的顺序,
    • 键可以是任何类型,而不仅仅是字符串。
  • 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 automaticallyconst 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());

只是为了添加到Scott Saurett的回答中,有些人在评论中询问如何使用他的函数来分组值1、值2等,而不是只分组一个值。

只需要编辑他的和函数:

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

根据之前的答案

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处的当前iteree值计算出来的,其值是该prop的所有值以及当前值的列表。

var arr = [{ Phase: "Phase 1", `enter code here`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" }];

创建并清空对象。循环arr并添加use阶段作为obj的唯一键。循环arr时继续更新obj中的关键总数。

const obj = {};arr.forEach((item) => {obj[item.Phase] = obj[item.Phase] ? obj[item.Phase] +parseInt(item.Value) : parseInt(item.Value);});

结果将看起来像这样:

{ "Phase 1": 50, "Phase 2": 130 }

通过obj循环到窗体和结果类型

const resultArr = [];for (item in obj) {resultArr.push({ Phase: item, Value: obj[item] });}console.log(resultArr);

排序功能

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

来自@Mormb,@jmarceli答案和这篇文章

我利用JSON.stringify()作为group by的原始价值多个列的标识。

没有第三方

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

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>

我已经扩展了已接受的答案,包括按多个属性分组,然后添加thenby并使其纯功能性而没有突变。

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;};
data = [{id:1, name:'BMW'}, {id:2, name:'AN'}, {id:3, name:'BMW'}, {id:1, name:'NNN'}]key = 'id'//try by id or namedata.reduce((previous, current)=>{previous[current[key]] && previous[current[key]].length != 0 ? previous[current[key]].push(current) : previous[current[key]] = new Array(current)return previous;}, {})

基于@陈志立的原始想法,我修改了代码并使用打字稿创建了一个group by函数。

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),该回调聚合dublicates。

使用示例:

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

呼吁

groupBy(data, demoComparator, demoOnDublicate)

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

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

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

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

检查过的答案--只是浅层分组。很容易理解缩减。问题还提供了额外的聚合计算问题。

这是一个REAL GROUP BY for Array of Object,由一些字段组成,具有1)计算的键名和2)通过提供所需键的列表来级联分组的完整解决方案并将其唯一值转换为根键,例如SQLGROUP 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 fieldlet 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 grouped 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;
/*** Recursive grouping 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 ) );

您可以在数组上使用forEach并构造一组新的项。以下是如何使用FlowType注释来做到这一点

// @flow
export class Group<T> {tag: numberitems: 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 = taggroup.items.push(item)groups.push(group)
currentGroup = group}})
return groups}
export default groupBy

一个玩笑测试就像

// @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')})

ES6reduce基于版本版本,支持功能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 omittedconsole.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版本颠倒了分组并使用values作为keyskeys作为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))

为了简洁起见,备注:函数以简短的形式(一行)发布,并仅与想法相关。您可以扩展它们并添加额外的错误检查等。

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

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

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

// TESTlet 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']) );

通常我使用Lodash JavaScript实用程序库和预构建的groupBy()方法。它非常容易使用,请参阅更多详细信息这里

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

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

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

  • 在两个=>中,你都有一个免费的return

  • Array.prototype.reduce函数最多需要4个参数。这就是为什么要添加第五个参数,这样我们就可以使用默认值在参数声明级别为group(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;}, {});

编辑:

TS版本:

const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) =>list.reduce((previous, currentItem) => {const group = getKey(currentItem);if (!previous[group]) previous[group] = [];previous[group].push(currentItem);return previous;}, {} as Record<K, T[]>);
let x  = [{"id": "6","name": "SMD L13","equipmentType": {"id": "1","name": "SMD"}},{"id": "7","name": "SMD L15","equipmentType": {"id": "1","name": "SMD"}},{"id": "2","name": "SMD L1","equipmentType": {"id": "1","name": "SMD"}}];
function groupBy(array, property) {return array.reduce((accumulator, current) => {const object_property = current[property];delete current[property]
let classified_element = accumulator.find(x => x.id === object_property.id);let other_elements = accumulator.filter(x => x.id !== object_property.id);
if (classified_element) {classified_element.children.push(current)} else {classified_element = {...object_property,'children': [current]}}return [classified_element, ...other_elements];}, [])}
console.log( groupBy(x, 'equipmentType') )
/* output
[{"id": "1","name": "SMD","children": [{"id": "6","name": "SMD L13"},{"id": "7","name": "SMD L15"},{"id": "2","name": "SMD L1"}]}]
*/

我会检查声明式jsgroupBy,它似乎完全符合您的要求。它也是:

  • 非常高性能(性能基准
  • 用打字稿写的,所以所有打字都包括在内。
  • 它不强制使用类似第三方数组的对象。
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());

简单的如果你使用Lodash库

let temp = []_.map(yourCollectionData, (row) => {let index = _.findIndex(temp, { 'Phase': row.Phase })if (index > -1) {temp[index].Value += row.Value} else {temp.push(row)}})

想象一下你有这样的东西:

[{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. 现在重复项消失了,我们将使用扩展运算符将其转换回数组…

设置Doc: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

function groupBy(array, groupBy){return array.reduce((acc,curr,index,array) => {var  idx = curr[groupBy];if(!acc[idx]){acc[idx] = array.filter(item => item[groupBy] === idx)}return  acc;
},{})}
// callgroupBy(items,'Step')

我有改进的答案。此函数接受组字段数组并返回分组对象,其键也是组字段的对象。

function(xs, groupFields) {groupFields = [].concat(groupFields);return xs.reduce(function(rv, x) {let groupKey = groupFields.reduce((keyObject, field) => {keyObject[field] = x[field];return keyObject;}, {});(rv[JSON.stringify(groupKey)] = rv[JSON.stringify(groupKey)] || []).push(x);return rv;}, {});}


let x = [{"id":1,"multimedia":false,"language":["tr"]},{"id":2,"multimedia":false,"language":["fr"]},{"id":3,"multimedia":true,"language":["tr"]},{"id":4,"multimedia":false,"language":[]},{"id":5,"multimedia":false,"language":["tr"]},{"id":6,"multimedia":false,"language":["tr"]},{"id":7,"multimedia":false,"language":["tr","fr"]}]
groupBy(x, ['multimedia','language'])
//{//{"multimedia":false,"language":["tr"]}: Array(3),//{"multimedia":false,"language":["fr"]}: Array(1),//{"multimedia":true,"language":["tr"]}: Array(1),//{"multimedia":false,"language":[]}: Array(1),//{"multimedia":false,"language":["tr","fr"]}: Array(1)//}

发帖是因为即使这个问题已经7年了,我还没有看到一个满足原始标准的答案:

我不希望他们“分裂”,而是“合并”,更像是SQL方法。

我最初写这篇文章是因为我想找到一种方法来减少对象数组(例如,从csv读取时创建的数据结构)并通过给定索引聚合以产生相同的数据结构。我正在寻找的返回值是另一个对象数组,而不是我在这里看到的嵌套对象或映射。

以下函数接受一个数据集(对象数组)、一个索引列表(数组)和一个缩减函数,并将缩减函数应用于索引的结果作为对象数组返回。

function agg(data, indices, reducer) {
// helper to create unique index as an arrayfunction getUniqueIndexHash(row, indices) {return indices.reduce((acc, curr) => acc + row[curr], "");}
// reduce data to single object, whose values will be each of the new rows// structure is an object whose values are arrays// [{}] -> \{\{}}// no operation performed, simply groupinglet groupedObj = data.reduce((acc, curr) => {let currIndex = getUniqueIndexHash(curr, indices);
// if key does not exist, create array with current rowif (!Object.keys(acc).includes(currIndex)) {acc = {...acc, [currIndex]: [curr]}// otherwise, extend the array at currIndex} else {acc = {...acc, [currIndex]: acc[currIndex].concat(curr)};}
return acc;}, {})
// reduce the array into a single object by applying the reducerlet reduced = Object.values(groupedObj).map(arr => {// for each sub-array, reduce into single object using the reducer functionlet reduceValues = arr.reduce(reducer, {});
// reducer returns simply the aggregates - add in the indices here// each of the objects in "arr" has the same indices, so we take the firstlet indexObj = indices.reduce((acc, curr) => {acc = {...acc, [curr]: arr[0][curr]};return acc;}, {});
reduceValues = {...indexObj, ...reduceValues};

return reduceValues;});

return reduced;}

我将创建一个返回计数(*)和求和(值)的减速器:

reducer = (acc, curr) => {acc.count = 1 + (acc.count || 0);acc.value = +curr.Value + (acc.value|| 0);return acc;}

最后,将agg函数和我们的简化器应用于原始数据集会产生一个应用了适当聚合的对象数组:

agg(tasks, ["Phase"], reducer);// yields:Array(2) [0: Object {Phase: "Phase 1", count: 4, value: 50}1: Object {Phase: "Phase 2", count: 4, value: 130}]
agg(tasks, ["Phase", "Step"], reducer);// yields:Array(4) [0: Object {Phase: "Phase 1", Step: "Step 1", count: 2, value: 15}1: Object {Phase: "Phase 1", Step: "Step 2", count: 2, value: 35}2: Object {Phase: "Phase 2", Step: "Step 1", count: 2, value: 55}3: Object {Phase: "Phase 2", Step: "Step 2", count: 2, value: 75}]

我不认为给出的答案是对问题的回应,我认为下面应该回答第一部分:

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

var newArr = data.reduce((acc, cur) => {const existType = acc.find(a => a.Phase === cur.Phase);if (existType) {existType.Value += +cur.Value;return acc;}
acc.push({Phase: cur.Phase,Value: +cur.Value});return acc;}, []);

GroupBy单行ES2021解决方案

const groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});

TypeScript

const groupBy = <T>(array: T[], predicate: (value: T, index: number, array: T[]) => string) =>array.reduce((acc, value, index, array) => {(acc[predicate(value, index, array)] ||= []).push(value);return acc;}, {} as { [key: string]: T[] });

示例

const groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});// f -> should must return string/number because it will be use as key in object
// for demo
groupBy([1, 2, 3, 4, 5, 6, 7, 8, 9], v => (v % 2 ? "odd" : "even"));// { odd: [1, 3, 5, 7, 9], even: [2, 4, 6, 8] };const colors = ["Apricot","Brown","Burgundy","Cerulean","Peach","Pear","Red",];
groupBy(colors, v => v[0]); // group by colors name first letter// {//   A: ["Apricot"],//   B: ["Brown", "Burgundy"],//   C: ["Cerulean"],//   P: ["Peach", "Pear"],//   R: ["Red"],// };groupBy(colors, v => v.length); // group by length of color names// {//   3: ["Red"],//   4: ["Pear"],//   5: ["Brown", "Peach"],//   7: ["Apricot"],//   8: ["Burgundy", "Cerulean"],// }
const data = [{ comment: "abc", forItem: 1, inModule: 1 },{ comment: "pqr", forItem: 1, inModule: 1 },{ comment: "klm", forItem: 1, inModule: 2 },{ comment: "xyz", forItem: 1, inModule: 2 },];
groupBy(data, v => v.inModule); // group by module// {//   1: [//     { comment: "abc", forItem: 1, inModule: 1 },//     { comment: "pqr", forItem: 1, inModule: 1 },//   ],//   2: [//     { comment: "klm", forItem: 1, inModule: 2 },//     { comment: "xyz", forItem: 1, inModule: 2 },//   ],// }
groupBy(data, x => x.forItem + "-" + x.inModule); // group by module with item// {//   "1-1": [//     { comment: "abc", forItem: 1, inModule: 1 },//     { comment: "pqr", forItem: 1, inModule: 1 },//   ],//   "1-2": [//     { comment: "klm", forItem: 1, inModule: 2 },//     { comment: "xyz", forItem: 1, inModule: 2 },//   ],// }

分组图

const groupByToMap = (x, f) =>x.reduce((a, b, i, x) => {const k = f(b, i, x);a.get(k)?.push(b) ?? a.set(k, [b]);return a;}, new Map());

TypeScript

const groupByToMap = <T, Q>(array: T[], predicate: (value: T, index: number, array: T[]) => Q) =>array.reduce((map, value, index, array) => {const key = predicate(value, index, array);map.get(key)?.push(value) ?? map.set(key, [value]);return map;}, new Map<Q, T[]>());

/*** array group by* @category array* @function arrayGroupBy* @returns  {object} {"fieldName":[{...}],...}* @static* @author hht* @param {string}} key group key* @param {array} data array** @example example 01* --------------------------------------------------------------------------* import { arrayGroupBy } from "@xx/utils";* const array =  [*  {*    type: 'assets',*    name: 'zhangsan',*    age: '33',*  },*  {*    type: 'config',*    name: 'a',*    age: '13',*  },*  {*    type: 'run',*    name: 'lisi',*    age: '3',*  },*  {*    type: 'xx',*    name: 'timo',*    age: '4',*  },*];* arrayGroupBy(array,'type',);** result:{*    assets: [{ age: '33', name: 'zhangsan', type: 'assets' }],*    config: [{ age: '13', name: 'a', type: 'config' }],*    run: [{ age: '3', name: 'lisi', type: 'run' }],*    xx: [{ age: '4', name: 'timo', type: 'xx' }],*  };** @example example 02 null* --------------------------------------------------------------------------* const array = null;* arrayGroupBy(array,"type");** result:{}** @example example 03 key undefind* --------------------------------------------------------------------------* const array =  [*  {*    type: 'assets',*    name: 'zhangsan',*    age: '33',*  },*  {*    type: 'config',*    name: 'a',*    age: '13',*  },*  {*    type: 'run',*    name: 'lisi',*    age: '3',*  },*  {*    type: 'xx',*    name: 'timo',*    age: '4',*  },*];* arrayGroupBy(array,"xx");** {}**/const arrayGroupBy = (data, key) => {if (!data || !Array.isArray(data)) return {};const groupObj = {};data.forEach((item) => {if (!item[key]) return;const fieldName = item[key];if (!groupObj[fieldName]) {groupObj[fieldName] = [item];return;}groupObj[fieldName].push(item);});return groupObj;};
const array = [{type: 'assets',name: 'zhangsan',age: '33',},{type: 'config',name: 'a',age: '13',},{type: 'run',name: 'lisi',age: '3',},{type: 'run',name: 'wangmazi',age: '3',},{type: 'xx',name: 'timo',age: '4',},];console.dir(arrayGroupBy(array, 'type'))
<p>

describe('arrayGroupBy match', () => {const array = [{type: 'assets',name: 'zhangsan',age: '33',},{type: 'config',name: 'a',age: '13',},{type: 'run',name: 'lisi',age: '3',},{type: 'xx',name: 'timo',age: '4',},];
test('arrayGroupBy  ...', () => {const result = {assets: [{ age: '33', name: 'zhangsan', type: 'assets' }],config: [{ age: '13', name: 'a', type: 'config' }],run: [{ age: '3', name: 'lisi', type: 'run' }],xx: [{ age: '4', name: 'timo', type: 'xx' }],};
expect(arrayGroupBy(array, 'type')).toEqual(result);});
test('arrayGroupBy not match..', () => {// resultexpect(arrayGroupBy(array, 'xx')).toEqual({});});
test('arrayGroupBy null', () => {let array = null;expect(arrayGroupBy(array, 'type')).toEqual({});});
test('arrayGroupBy undefined', () => {let array = undefined;expect(arrayGroupBy(array, 'type')).toEqual({});});
test('arrayGroupBy empty', () => {let array = [];expect(arrayGroupBy(array, 'type')).toEqual({});});});
</p>

使用ES6的简单解决方案:

该方法有一个返回模型,允许比较n个属性。

const compareKey = (item, key, compareItem) => {return item[key] === compareItem[key]}
const handleCountingRelatedItems = (listItems, modelCallback, compareKeyCallback) => {return listItems.reduce((previousValue, currentValue) => {if (Array.isArray(previousValue)) {const foundIndex = previousValue.findIndex(item => compareKeyCallback(item, currentValue))
if (foundIndex > -1) {const count = previousValue[foundIndex].count + 1
previousValue[foundIndex] = modelCallback(currentValue, count)
return previousValue}
return [...previousValue, modelCallback(currentValue, 1)]}
if (compareKeyCallback(previousValue, currentValue)) {return [modelCallback(currentValue, 2)]}
return [modelCallback(previousValue, 1), modelCallback(currentValue, 1)]})}
const itemList = [{ type: 'production', human_readable: 'Production' },{ type: 'test', human_readable: 'Testing' },{ type: 'production', human_readable: 'Production' }]
const model = (currentParam, count) => ({label: currentParam.human_readable,type: currentParam.type,count})
const compareParameter = (item, compareValue) => {const isTypeEqual = compareKey(item, 'type', compareValue)return isTypeEqual}
const result = handleCountingRelatedItems(itemList, model, compareParameter)
console.log('Result: \n', result)/** Result:[{ label: 'Production', type: 'production', count: 2 },{ label: 'Testing', type: 'testing', count: 1 }]*/

根据Joseph Nields的回答,在https://github.com/padcom/array-prototype-functions#arrayprototypegroupbyfieldormapper中有一个用于分组对象的多边形填充。所以与其一遍又一遍地写,你可能想使用已经可用的东西。

在我的特定用例中,我需要按属性分组,然后删除分组属性。

无论如何,该属性只是出于分组目的添加到记录中,对于向用户呈现没有意义。

    group (arr, key) {
let prop;
return arr.reduce(function(rv, x) {prop = x[key];delete x[key];(rv[prop] = (rv[prop] || [])).push(x);return rv;}, {});
},

感谢@caesar-bautista在顶部答案中的启动功能。

让我们在重用已经编写的代码(即Underscore)的同时完全回答最初的问题。如果你结合它>100个函数,你可以使用Underscore做更多的事情。以下解决方案演示了这一点。

第1步:通过属性的任意组合对数组中的对象进行分组。这使用了_.groupBy接受返回对象组的函数这一事实。它还使用了_.chain_.pick_.chain0、_.chain1和_.chain2。请注意,这里并不严格需要_.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_.first0、_.first1、_.first2、_.first3和_.first4。在这种情况下,_.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,我们得到了asker想要的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" ] }]

归功于@much2Learning,他还发布了一个可以处理任意约简函数的回答。我又写了几个SO答案,展示了如何通过组合多个Underscore函数来实现复杂的事情:

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

有点晚了,但也许有人喜欢这个。

ES6:

const users = [{name: "Jim",color: "blue"},{name: "Sam",color: "blue"},{name: "Eddie",color: "green"},{name: "Robert",color: "green"},];const groupBy = (arr, key) => {const initialValue = {};return arr.reduce((acc, cval) => {const myAttribute = cval[key];acc[myAttribute] = [...(acc[myAttribute] || []), cval]return acc;}, initialValue);};
const res = groupBy(users, "color");console.log("group by:", res);

您可以使用原生JavaScriptgroup数组方法(目前处于第三阶段)。

我认为解决方案要优雅得多,相比减少,或接触第三方库,如洛达什等。

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>

如果你需要做多组:

const populate = (entireObj, keys, item) => {let keysClone = [...keys],currentKey = keysClone.shift();
if (keysClone.length > 0) {entireObj[item[currentKey]] = entireObj[item[currentKey]] || {}populate(entireObj[item[currentKey]], keysClone, item);} else {(entireObj[item[currentKey]] = entireObj[item[currentKey]] || []).push(item);}}
export const groupBy = (list, key) => {return list.reduce(function (rv, x) {
if (typeof key === 'string') (rv[x[key]] = rv[x[key]] || []).push(x);
if (typeof key === 'object' && key.length) populate(rv, key, x);
return rv;
}, {});}
const myPets = [{name: 'yaya', type: 'cat', color: 'gray'},{name: 'bingbang', type: 'cat', color: 'sliver'},{name: 'junior-bingbang', type: 'cat', color: 'sliver'},{name: 'jindou', type: 'cat', color: 'golden'},{name: 'dahuzi', type: 'dog', color: 'brown'},];
// rungroupBy(myPets, ['type', 'color']));
// you will get object like:
const afterGroupBy = {"cat": {"gray": [{"name": "yaya","type": "cat","color": "gray"}],"sliver": [{"name": "bingbang","type": "cat","color": "sliver"},{"name": "junior-bingbang","type": "cat","color": "sliver"}],"golden": [{"name": "jindou","type": "cat","color": "golden"}]},"dog": {"brown": [{"name": "dahuzi","type": "dog","color": "brown"}]}};

解释相同的代码。喜欢它我喜欢它这里

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

这是一个基于TS的函数,不是最高性能的,但很容易阅读和遵循!

function groupBy<T>(array: T[], key: string): Record<string, T[]> {const groupedObject = {}for (const item of array) {const value = item[key]if (groupedObject[value] === undefined) {groupedObject[value] = []}groupedObject[value].push(item)}return groupedObject}

我们以这样的方式结束->

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" },];console.log(groupBy(data, 'Step')){'Step 1': [{Phase: 'Phase 1',Step: 'Step 1',Task: 'Task 1',Value: '5'},{Phase: 'Phase 1',Step: 'Step 1',Task: 'Task 2',Value: '10'}],'Step 2': [{Phase: 'Phase 1',Step: 'Step 2',Task: 'Task 1',Value: '15'},{Phase: 'Phase 1',Step: 'Step 2',Task: 'Task 2',Value: '20'}]}