什么是最有效的方法来groupby一个JavaScript数组的对象?

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

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

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

我在表格中显示这些信息。 我想分组不同的方法,但我想总结的价值观。

我为它的groupby函数使用了underscore.js,这是有帮助的,但是并没有做到这一点,因为我不希望它们“分裂”,而是“合并”,更像是SQL-by方法。

我正在寻找的将能够完成具体的价值(如果要求)。

所以如果我做了groupby阶段,我想要收到:

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

如果我做了一些合适的阶段/步骤,我会收到:

 [ { 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版本:

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

虽然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的行动 。

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

现在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这里只是一个潜在的function。 您可以按照自己的喜好注册其他人:

 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接受一个函数并创build一个接受初始数据和要分组的属性的新函数。 然后这个新的函数按照上面的输出格式,依次运行你的函数,返回一个新的数组。 生成的函数根据您提供的名称作为DataGrouper一个属性存储,如果您只需要本地引用,也会返回该属性。

那么这是很多的解释。 代码是相当直接的,我希望!

使用ES6地图对象:

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

用法示例:

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

jsfiddle: https ://jsfiddle.net/buko8r5d/

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

这可能更容易使用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 } ] 

或者,更简单地使用基于string的select器( DEMO ):

 linq.GroupBy("$.Phase", "", "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray(); 
 _.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo'); >> Object {A: Array[2], B: Array[1]} 

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

我会检查lodash组似乎是做你正在寻找什么。 它也相当轻巧,非常简单。

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

假设您的数组名是arr ,那么使用lodash的groupBy就是:

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

你可以用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这个例子。

顺便说一句:在大型arrays(10万条logging和更多)的Alasql更快塔姆Linq。 参见jsPref的testing。

注释:

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

我想build议我的方法。 首先,单独分组和汇总。 让我们声明原型“group by”函数。 它需要另一个函数来为数组元素产生“散列”string。

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

虽然这个问题有一些答案,答案看起来有点复杂,我build议使用香草Javascript的分组。

此解决scheme的特点是一个函数,该函数使用一个数组array和一个属性名称作为返回col并使用一个属性名称来计算值的value

该函数依赖于一个对象,该对象充当结果的散列表。

 function groupBy(array, col, value) { var r = [], o = {}; array.forEach(function (a) { if (!o[a[col]]) { o[a[col]] = {}; o[a[col]][col] = a[col]; o[a[col]][value] = 0; r.push(o[a[col]]); } o[a[col]][value] += +a[value]; }); 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" }]; document.write('<pre>' + JSON.stringify(groupBy(data, 'Phase', 'Value'), 0, 4) + '</pre>'); 

这个解决scheme使用任意函数(而不是一个键),因此比上面的解决scheme更加灵活,并且允许使用箭头函数 ,这与LINQ中使用的lambdaexpression式类似:

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

使用箭头function(ES6)的示例:

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

以上两个例子都返回:

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

让我们生成一个通用的Array.prototype.groupBy()工具。 只是为了多样化,让我们使用ES6 fanciness传播运算符为一些Haskellesque模式匹配的recursion方法。 还让我们让我们的Array.prototype.groupBy()接受一个callback,它将项目( 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); 

没有突变:

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

与ES6:

 const groupBy = (items, key) => items.reduce( (result, item) => ({ ...result, [item[key]]: [ ...(result[item[key]] || []), item, ], }), {}, ); 
 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; }, []); } 

这一个输出数组。

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

我从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; })); 

Ceasar的答案是好的,但只适用于数组内部元素的内部属性(在string的情况下为长度)。

这个实现工作更像: https : //lodash.com/docs/4.17.4#groupBy

 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版本,不会在null成员上中断

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