对对象数组进行分组的最有效方法
Posted
技术标签:
【中文标题】对对象数组进行分组的最有效方法【英文标题】:Most efficient method to groupby on an array of objects 【发布时间】:2022-01-18 03:20:25 【问题描述】:对数组中的对象进行分组的最有效方法是什么?
例如,给定这个对象数组:
[
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 @987654323 @方法。
我正在寻找的内容能够汇总特定值(如果需要)。
因此,如果我使用 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,然后循环遍历生成的对象以自己进行总计?
【问题讨论】:
虽然 _.groupBy 自己不能完成这项工作,但它可以与其他 Underscore 函数结合来完成所要求的工作。无需手动循环。看到这个答案:***.com/a/66112210/1166087。 已接受答案的可读性更强的版本: function groupBy(data, key) return data.reduce( (acc, cur) => acc[cur[key]] = acc[cur[key]] || []; // if the key is new, initiate its value to an array, otherwise keep its own array value acc[cur[key]].push(cur); return acc; , [])
【参考方案1】:
如果你想避免使用外部库,你可以像这样简洁地实现 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"]
【讨论】:
我会这样修改:``` return xs.reduce(function(rv, x) var v = key instanceof Function ? key(x) : x[key]; (rv[v ] = rv[v] || []).push(x); 返回 rv; , ); ``` 允许回调函数返回排序标准 这里输出数组而不是对象: 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, 值: [x] ); 返回 rv; , []); 太好了,正是我需要的。如果其他人需要它,这里是 TypeScript 签名:var groupBy = function<TItem>(xs: TItem[], key: string) : [key: string]: TItem[] ...
如果有人感兴趣,我制作了这个函数的更易读和注释的版本,并将其放在一个要点中:gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d 我发现它有助于理解这里实际发生的事情。
我们不能有合理的变量名吗?【参考方案2】:
使用 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
【讨论】:
@mortb,如何在不调用get()
方法的情况下得到它?这是我希望在不传递密钥的情况下显示输出
@FaiZalDong:我不确定什么最适合您的情况?如果我在 jsfiddle 示例中编写 console.log(grouped.entries());
,它会返回一个可迭代的,其行为类似于键 + 值的数组。你可以试试看是否有帮助?
你也可以试试console.log(Array.from(grouped));
查看组中元素的数量:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( [i[0]]: i[1].length ))
我已将 jsfiddle 转换为 *** 内联代码 sn-p。原jsFiddle还在网上:jsfiddle.net/buko8r5d【参考方案3】:
使用 ES6:
const groupBy = (items, key) => items.reduce(
(result, item) => (
...result,
[item[key]]: [
...(result[item[key]] || []),
item,
],
),
,
);
【讨论】:
这需要一点时间来适应,但大多数 C++ 模板也是如此 我绞尽脑汁,仍然无法理解它是如何从...result
开始工作的。现在我因此无法入睡。
优雅,但在较大的阵列上速度很慢!
@user3307073 我认为乍一看...result
是起始值,这就是为什么它如此令人困惑(如果我们还没有开始构建result
,...result
是什么? )。但起始值是.reduce()
的第二个参数,而不是第一个参数,它位于底部:
。所以你总是从一个 JS 对象开始。相反,...result
位于传递给第一个参数的
中,因此这意味着“从您已有的所有字段开始(在添加新字段 item[key]
之前)”。
@ArthurTacca 你是对的,result
是累加器,这意味着它是每个项目更新的“工作值”。它以空对象开始,每个项目都添加到分配给具有分组字段值名称的属性的数组中。【参考方案4】:
你可以从 array.reduce()
构建一个 ES6 Map
。
const groupedMap = initialArray.reduce(
(entryMap, e) => entryMap.set(e.id, [...entryMap.get(e.id)||[], e]),
new Map()
);
与其他解决方案相比,这有一些优势:
它不需要任何库(不像_.groupBy()
)
你得到一个 JavaScript Map
而不是一个对象(例如由 _.groupBy()
返回)。这有lots of benefits,包括:
它会记住第一次添加项目的顺序,
键可以是任何类型,而不仅仅是字符串。
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()
);
【讨论】:
我不知道为什么这没有更多的选票。它简洁、易读(对我而言)并且看起来高效。 It doesn't fly on IE11,但改造并不难(a.reduce(function(em, e)em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;, new Map())
,大约)【参考方案5】:
我会检查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
【讨论】:
这个答案是不是有问题?有多种方式导致 lodash _.groupBy 结果不是 OP 请求的结果格式。 (1) 结果不是数组。 (2) “值”已成为 lodash 对象结果中的“键”。 为了更简单,你可以直接将属性传递给groupBy:const a = groupBy(arr, 'Phase')
【参考方案6】:
虽然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));
);
你可以看到in action on JSBin。
我在 Underscore 中没有看到任何与 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
的属性,如果您只需要本地引用,也会返回。
嗯,这是很多解释。我希望代码相当简单!
【讨论】:
嗨.. 可以看到你只按一个值分组和求和,但如果我想按 value1 和 value2 和 value3 求和...你有解决方案吗? @SAMUELOSPINA 你有没有找到办法做到这一点?【参考方案7】:使用linq.js
可能更容易做到这一点,它旨在成为 JavaScript 中 LINQ 的真正实现 (DEMO):
var linq = Enumerable.From(data);
var result =
linq.GroupBy(function(x) return x.Phase; )
.Select(function(x)
return
Phase: x.Key(),
Value: x.Sum(function(y) return y.Value|0; )
;
).ToArray();
结果:
[
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; )
linq.js 性能如何?【参考方案8】:
GroupBy one-liner,ES2021 解决方案
const groupBy = (x,f)=>x.reduce((a,b)=>((a[f(b)]||=[]).push(b),a),);
打字稿
const groupBy = <T>(array: T[], predicate: (v: T) => string) =>
array.reduce((acc, value) =>
(acc[predicate(value)] ||= []).push(value);
return acc;
, as [key: string]: T[] );
示例
const groupBy = (x, f) => x.reduce((a, b) => ((a[f(b)] ||= []).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 ,
// ],
//
【讨论】:
||= 被我的 Babel 拒绝了? 最近才标准化。 blog.saeloun.com/2021/06/17/… 我爱我,我的简洁需要更多时间来弄清楚魔术单线!迄今为止最(主观)优雅的解决方案。 非常优雅,尤其是能够以这种方式调整谓词。华丽。【参考方案9】:MDN 在其Array.reduce()
文档中有this example。
// 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 ]
//
【讨论】:
我显然遗漏了一些东西。为什么我们不能用 MDN 的这个解决方案生成一个数组?如果你尝试用 ,[] 初始化 reducer,你会得到一个空数组。【参考方案10】:_.groupBy([tipo: 'A' ,tipo: 'A', tipo: 'B'], 'tipo');
>> Object A: Array[2], B: Array[1]
发件人:http://underscorejs.org/#groupBy
【讨论】:
【参考方案11】: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]
;
);
;
【讨论】:
【参考方案12】:您可以使用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]);
试试这个例子at jsFiddle。
顺便说一句: 在大型数组(100000 条记录及更多)上,Alasql 比 Linq 更快。见测试at jsPref。
评论:
这里我把Value放在方括号里,因为VALUE是SQL中的关键字 我必须使用 CAST() 函数将字符串值转换为数字类型。【讨论】:
【参考方案13】:一种较新的方法,其中包含一个用于分组的对象和另外两个函数来创建一个键并获取一个具有所需分组项的对象和另一个用于增加值的键。
const
groupBy = (array, groups, valueKey) =>
const
getKey = 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;
旧方法:
虽然问题有一些答案,而且答案看起来有点复杂,但我建议使用带有嵌套(如果需要)Map
的 vanilla Javascript 进行分组。
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;
【讨论】:
【参考方案14】:有点晚了,但也许有人喜欢这个。
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);
【讨论】:
谢谢你的方法有效,我对这个概念有点新,你能解释一下 initialValue 部分它做了什么【参考方案15】:这是一个使用 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;
, );
编辑:
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[]>);
【讨论】:
你能解释一下吗,它很完美 @NuwanDammika - 在这两个 => 中你都有一个免费的“返回” - reduce 函数最多需要 4 个参数。这就是为什么要添加第五个参数,以便我们可以为组 (k) 提供一个廉价的变量声明。 - 如果之前的值没有我们当前的组,我们创建一个新的空组 ((r[k] || (r[k] = [])) 这将评估最左边的表达式,否则为数组或空数组,这就是为什么在该表达式之后立即推送的原因。 - 当有返回时,逗号运算符将丢弃最左边的值,返回调整后的前一组。 TS 的最佳语法。与复杂对象一起使用时的最佳答案。const groups = groupBy(items, (x) => x.groupKey);
这很棒。我是一个 scala 人,感觉就像在家里一样。嗯.. 除了 default 是什么?
@javadba export default 只是用于 JS 模块的语法,类似于仅导出,默认关键字将允许您像这样导入:import Group from '../path/to/模块';【参考方案16】:
检查过的答案 - 只是浅分组。理解减少非常好。问题还提供了额外聚合计算的问题。
这是一个 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 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 ) );
【讨论】:
【参考方案17】:此解决方案采用任意函数(不是键),因此比上述解决方案更灵活,并允许arrow functions,类似于 LINQ 中使用的lambda expressions:
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;);
【参考方案18】:
没有突变:
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"]
【讨论】:
【参考方案19】:我想建议我的方法。首先,单独分组和聚合。让我们声明原型的“分组依据”功能。它需要另一个函数来为要分组的每个数组元素生成“哈希”字符串。
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 控制台中测试了这段代码。并随时改进和发现错误;)
【讨论】:
谢谢!喜欢这种方法,并且非常适合我的需求(我不需要聚合)。 我想你想把你的行在 put():map[_hash(key)].key = key;
改为 map[_hash(key)].key = _hash(key);
。
请注意,如果数组包含名称类似于对象原型中任何函数的字符串(例如:["toString"].groupBy()
),这将失败【参考方案20】:
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;
, []);
这个输出数组。
【讨论】:
【参考方案21】:想象一下你有这样的东西:
[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 只允许唯一值,所以所有重复的都将被删除。
-
现在重复项消失了,我们将使用扩展运算符将其转换回数组...
设置文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set 传播 OperatorDoc:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
【讨论】:
我很喜欢你的回答,是最短的,但我还是不明白其中的逻辑,尤其是这里的分组是谁?它是传播运算符(...)吗?还是“新 Set()”?请向我们解释一下...谢谢 1.首先,我们通过传递一个数组来创建一个新的 Set。因为 Set 只允许唯一值,所以所有重复项都将被删除。 2. 现在重复项都没有了,我们将使用扩展运算符将其转换回数组... Set Doc:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Spread Operator:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…【参考方案22】:根据之前的回答
const groupBy = (prop) => (xs) =>
xs.reduce((rv, x) =>
Object.assign(rv, [x[prop]]: [...(rv[x[prop]] || []), x]), );
如果您的环境支持,使用对象扩展语法会更好看。
const groupBy = (prop) => (xs) =>
xs.reduce((acc, x) => (
...acc,
[ x[ prop ] ]: [...( acc[ x[ prop ] ] || []), x],
), );
这里,我们的 reducer 采用部分形成的返回值(从一个空对象开始),并返回一个由前一个返回值的展开成员组成的对象,以及一个新成员,其键是从当前返回值计算出来的iteree 在 prop
的值,其值是该道具的所有值以及当前值的列表。
【讨论】:
【参考方案23】:我不认为给出的答案是对问题的回应,我认为以下内容应该回答第一部分:
const arr = [
Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" ,
Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" ,
Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" ,
Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" ,
Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" ,
Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" ,
Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" ,
Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40"
]
const groupBy = (key) => arr.sort((a, b) => a[key].localeCompare(b[key])).reduce((total, currentValue) =>
const newTotal = total;
if (
total.length &&
total[total.length - 1][key] === currentValue[key]
)
newTotal[total.length - 1] =
...total[total.length - 1],
...currentValue,
Value: parseInt(total[total.length - 1].Value) + parseInt(currentValue.Value),
;
else newTotal[total.length] = currentValue;
return newTotal;
, []);
console.log(groupBy('Phase'));
// => [ Phase: "Phase 1", Value: 50 , Phase: "Phase 2", Value: 130 ]
console.log(groupBy('Step'));
// => [ Step: "Step 1", Value: 70 , Step: "Step 2", Value: 110 ]
【讨论】:
键 Step Ex 的错误输出:groupBy('Step') 是的,确切地说,我认为您必须先对其进行排序: arr.sort((a, b) => a[key] - b[key]).reduce... 我更新了我的答案 我的错:sort((a, b) => a[key].localeCompare(b[key]))【参考方案24】:让我们在重用已编写的代码(即下划线)的同时全面回答原始问题。如果你结合它的 >100 个函数,你可以用 Underscore 做更多的事情。以下解决方案演示了这一点。
第 1 步:通过属性的任意组合对数组中的对象进行分组。这使用了_.groupBy
接受返回对象组的函数这一事实。它还使用_.chain
、_.pick
、_.values
、_.join
和_.value
。请注意 _.value
在这里不是严格需要的,因为链式值在用作属性名称时会自动展开。我将它包括在内是为了防止有人试图在不发生自动展开的上下文中编写类似的代码。
// Given an object, return a string naming the group it belongs to.
function category(obj)
return _.chain(obj).pick(propertyNames).values().join(' ').value();
// Perform the grouping.
const intermediate = _.groupBy(arrayOfObjects, category);
给定原始问题中的arrayOfObjects
并将propertyNames
设置为['Phase', 'Step']
,intermediate
将得到以下值:
"Phase 1 Step 1": [
Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" ,
Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10"
],
"Phase 1 Step 2": [
Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" ,
Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20"
],
"Phase 2 Step 1": [
Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" ,
Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30"
],
"Phase 2 Step 2": [
Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" ,
Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40"
]
第 2 步:将每个组缩减为单个平面对象,并将结果以数组的形式返回。除了我们之前看到的函数之外,下面的代码还使用了_.pluck
、_.first
、_.pick
、_.extend
、_.reduce
和_.map
。在这种情况下,_.first
保证返回一个对象,因为_.groupBy
不会产生空组。在这种情况下,_.value
是必需的。
// Sum two numbers, even if they are contained in strings.
const addNumeric = (a, b) => +a + +b;
// Given a `group` of objects, return a flat object with their common
// properties and the sum of the property with name `aggregateProperty`.
function summarize(group)
const valuesToSum = _.pluck(group, aggregateProperty);
return _.chain(group).first().pick(propertyNames).extend(
[aggregateProperty]: _.reduce(valuesToSum, addNumeric)
).value();
// Get an array with all the computed aggregates.
const result = _.map(intermediate, summarize);
鉴于我们之前获得的intermediate
并将aggregateProperty
设置为Value
,我们得到了提问者所需的result
:
[
Phase: "Phase 1", Step: "Step 1", Value: 15 ,
Phase: "Phase 1", Step: "Step 2", Value: 35 ,
Phase: "Phase 2", Step: "Step 1", Value: 55 ,
Phase: "Phase 2", Step: "Step 2", Value: 75
]
我们可以将这一切放在一个以arrayOfObjects
、propertyNames
和aggregateProperty
作为参数的函数中。请注意,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();
实际上,我们将部分责任转移给了调用者;她必须提供一个可以传递给_.reduce
的iteratee
,以便对_.reduce
的调用将生成一个具有她想要添加的聚合属性的对象。例如,我们通过以下表达式获得与之前相同的result
:
aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => (
Value: +memo.Value + +value.Value
));
对于稍微复杂的iteratee
的示例,假设我们想要计算每个组的最大值 Value
而不是总和,并且我们想要添加一个@987654388 @ 属性,列出组中出现的所有 Task
值。这是我们可以做到这一点的一种方法,使用上面的aggregate
(和_.union
)的最新版本:
aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => (
Value: Math.max(memo.Value, value.Value),
Tasks: _.union(memo.Tasks || [memo.Task], [value.Task])
));
我们得到以下结果:
[
Phase: "Phase 1", Step: "Step 1", Value: 10, Tasks: [ "Task 1", "Task 2" ] ,
Phase: "Phase 1", Step: "Step 2", Value: 20, Tasks: [ "Task 1", "Task 2" ] ,
Phase: "Phase 2", Step: "Step 1", Value: 30, Tasks: [ "Task 1", "Task 2" ] ,
Phase: "Phase 2", Step: "Step 2", Value: 40, Tasks: [ "Task 1", "Task 2" ]
]
感谢@much2learn,他还发布了一个answer,它可以处理任意归约函数。我又写了几个 SO 答案,展示了如何通过组合多个下划线函数来实现复杂的事情:
https://***.com/a/64938636/1166087 https://***.com/a/64094738/1166087 https://***.com/a/63625129/1166087 https://***.com/a/63088916/1166087【讨论】:
【参考方案25】: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[]>;
;
【讨论】:
我会对这个版本的性能基准(在每一轮使用新数组和解构以创建要设置的值)与另一个仅在需要时创建一个空数组的基准感兴趣。根据您的代码:gist.github.com/masonlouchart/da141b3af477ff04ccc626f188110f28 要明确一点,对于遇到这个问题的新手来说,这是 Typescript 代码,原来的问题被标记为 javascript,所以这有点离题了,对吧?【参考方案26】: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>
【讨论】:
【参考方案27】:我会检查 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());
【讨论】:
【参考方案28】:让我们生成一个通用的Array.prototype.groupBy()
工具。只是为了多样化,让我们使用 ES6 奇特的扩展运算符在递归方法上进行一些 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);
【讨论】:
【参考方案29】:为了补充 Scott Sauyet 的answer,一些人在 cmets 中询问如何使用他的函数对 value1、value2 等进行分组,而不是只对一个值进行分组。
只需编辑他的求和函数:
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;
());
【讨论】:
【参考方案30】:Ceasar 的回答很好,但仅适用于数组内部元素的内部属性(字符串的长度)。
这个实现更像是:this link
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;
, );
;
希望这会有所帮助...
【讨论】:
以上是关于对对象数组进行分组的最有效方法的主要内容,如果未能解决你的问题,请参考以下文章