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

Most efficient method to groupby on an array of objects

提问人:D'Arcy Rail-Ip 提问时间:1/22/2013 最后编辑:Paul RooneyD'Arcy Rail-Ip 更新时间:10/17/2023 访问量:1217308

问:

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

例如,给定以下对象数组:

[ 
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
]

我在表格中显示此信息。我想按不同的方法分组,但我想对值求和。

我正在使用 Underscore.js 作为它的 groupby 函数,这很有帮助,但并不能完成全部技巧,因为我不希望它们“拆分”而是“合并”,更像是 SQL 方法。group by

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

因此,如果我做了 groupby ,我希望收到:Phase

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

如果我做了 groupy / ,我会收到:PhaseStep

[
    { 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,然后遍历生成的对象自己做总计?

JavaScript 数组 对象 分组 by 下划线 .js

评论

0赞 Julian 2/9/2021
虽然 _.groupBy 本身不能完成这项工作,但它可以与其他 Underscore 函数结合使用,以执行所要求的操作。无需手动循环。请看这个答案:stackoverflow.com/a/66112210/1166087
0赞 aderchox 1/18/2022
接受答案的可读性更强的版本:­ 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; } , []) }

答:

52赞 mellamokb 1/22/2013 #1

使用 linq.js 可能更容易做到这一点,它旨在成为 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 }
]

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

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

评论

0赞 Amit 12/17/2015
我们可以在这里分组时使用多个属性吗?GroupBy(function(x){ return x.Phase; })
0赞 Rajon Tanducar 1/25/2022
linq.js 的性能如何?
63赞 Scott Sauyet 1/24/2013 #2

虽然 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 上看到它的实际效果

我在 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它本身就是一个函数。您可以使用您的数据和要作为分组依据的属性列表来调用它。它返回一个数组,其元素是具有两个属性的对象:是分组属性的集合,是包含不在键中的其余属性的对象数组。例如,将产生:keyvalsDataGrouper(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

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

评论

0赞 SAMUEL OSPINA 10/3/2016
你好。。可以看到你只按一个值分组和求和,但万一我想要按值 1 和 value2 和 value3 求和......你有解决方案吗?
0赞 howMuchCheeseIsTooMuchCheese 8/16/2017
你@SAMUELOSPINA找到一种方法来做到这一点?
8赞 Anton 10/25/2013 #3

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

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

评论

0赞 aberaud 3/2/2014
谢谢!喜欢这种方法,并且完全符合我的需求(我不需要聚合)。
0赞 Scotty.NET 3/3/2015
我想你想把你在 put(): 中的行改为 .map[_hash(key)].key = key;map[_hash(key)].key = _hash(key);
0赞 tigrou 11/4/2021
请注意,如果数组包含名称与对象原型中任何函数相似的字符串(例如:["toString"].groupBy())
25赞 Julio Marins 11/12/2014 #4
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

寄件人: http://underscorejs.org/#groupBy

20赞 agershun 12/23/2014 #5

你可以用 Alasql JavaScript 库来做到这一点:

var data = [ { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
             { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" }];

var res = alasql('SELECT Phase, Step, SUM(CAST([Value] AS INT)) AS [Value] \
                  FROM ? GROUP BY Phase, Step',[data]);

在 jsFiddle 中尝试此示例。

顺便说一句:在大型数组(100000 条记录或更多)上,Alasql 速度更快,tham Linq。参见 jsPref 的 test。

评论:

  • 这里我把 Value 放在方括号里,因为 VALUE 是 SQL 中的一个关键字
  • 我必须使用 CAST() 函数将字符串值转换为数字类型。
22赞 cezarypiatek 6/27/2015 #6
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]
        };
    });
};
80赞 jmarceli 1/8/2016 #7

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

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

假设您的数组名称是 groupBy,则 lodash 只是:arr

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

评论

4赞 mg1075 10/25/2018
这个答案不是有问题吗?有多种方式使 lodash _.groupBy 结果不是 OP 请求的结果格式。(1) 结果不是数组。(2)“值”已成为lodash对象结果中的“键”。
1赞 Ollie 11/6/2020
为了更简单起见,您可以直接将属性传递给 groupBy:const a = groupBy(arr, 'Phase')
1293赞 Ceasar 1/20/2016 #8

如果你想避免外部库,你可以简洁地实现一个这样的香草版本: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"]}

评论

28赞 y_nk 7/7/2016
我会这样修改: ''' return xs.reduce(function(rv, x) { var v = key instanceof Function ? key(x) : x[key];(rv[v] = rv[v] ||[]).push(x);返回房车;}, {});''' 允许回调函数返回排序条件
155赞 tomitrescak 8/3/2016
这是一个输出数组而不是对象: 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; }, []); }
49赞 Michael Sandino 12/7/2017
太好了,正是我需要的。如果其他人需要它,这里是 TypeScript 签名:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
57赞 robmathers 10/26/2018
如果有人感兴趣,我为这个函数制作了一个更具可读性和注释的版本,并将其放在一个要点中:gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d 我发现它有助于理解这里实际发生的事情。
93赞 HJo 7/1/2019
我们不能有理智的变量名称吗?
20赞 Nina Scholz 3/13/2016 #9

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

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

旧方法:

虽然这个问题有一些答案,而且答案看起来有点过于复杂,但我建议使用普通的 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; }

412赞 mortb 7/12/2016 #10

使用 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

评论

0赞 Fai Zal Dong 12/26/2017
@mortb,如何在不调用方法的情况下获取它?这是我想要的输出显示而不传递密钥get()
0赞 mortb 12/28/2017
@FaiZalDong:我不确定什么最适合你的情况?如果我在 jsfiddle 示例中编写,它会返回一个可迭代对象,其行为类似于键 + 值的数组。你能试试看是否有帮助吗?console.log(grouped.entries());
8赞 mortb 12/28/2017
您也可以尝试console.log(Array.from(grouped));
1赞 Ahmet Şimşek 3/12/2019
要查看组中的元素数,请执行以下操作:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
1赞 mortb 1/2/2023
@Ansharja是的,只要 key-getter 函数返回字符串,您就可以使用 JavaScript 对象而不是 a。阅读您的代码,我想指出,当仅调用字符串构造函数-- --将输出包装在 keygetter 函数中时,可能会导致意外后果,例如,当使用 a 作为键时,会出现一些边缘情况。MapString(keygetter)Date
8赞 tomitrescak 8/3/2016 #11
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;
    }, []);
}

这个输出数组。

8赞 Bless 4/5/2017 #12

无突变:

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

console.log(groupBy(['one', 'two', 'three'], 'length'));
// => {3: ["one", "two"], 5: ["three"]}
8赞 Diego 4/20/2017 #13

此解决方案采用任意函数(不是键),因此它比上述解决方案更灵活,并允许箭头函数,这类似于 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"}]
}

评论

0赞 caneta 4/28/2017
我非常喜欢 ES6 解决方案。在不扩展 Array 原型的情况下进行一些示例:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
167赞 Joseph Nields 9/27/2017 #14

使用 ES6:

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

评论

6赞 Levi Haskell 1/22/2019
这需要一点时间来适应,但大多数 C++ 模板也是如此
11赞 3/29/2019
我绞尽脑汁,仍然无法理解它到底是如何工作的。现在我因此无法入睡。...result
18赞 infinity1975 4/3/2019
优雅,但在较大的阵列上速度非常慢!
2赞 Arthur Tacca 8/14/2020
@user3307073 我认为乍一看就像是起始值,这就是为什么它如此令人困惑(如果我们还没有开始构建怎么办?但是起始值是 的第二个参数,而不是第一个参数,它在底部:。所以你总是从一个JS对象开始。取而代之的是,在传递给第一个参数的 that 中,因此它的意思是“从您已有的所有字段开始(在添加新字段之前)”。...result...resultresult.reduce(){}...result{}item[key]
1赞 Daniel 8/4/2021
@ArthurTacca你是对的,是累加器,这意味着它是每个项目更新的“工作值”。它以空对象开始,每个项目都添加到分配给属性的数组中,其名称为分组字段值。result
3赞 bigkahunaburger 10/29/2017 #15

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

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

评论

1赞 Rob Lyndon 10/4/2022
那些点差运营商确实赢得了他们的颜色。
126赞 Arthur Tacca 12/11/2017 #16

您可以从 构建 ES6。Maparray.reduce()

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

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

  • 它不需要任何库(不像例如_.groupBy())
  • 你得到的是一个 JavaScript 而不是一个对象(例如,返回者 )。这有很多好处,包括:Map_.groupBy()
    • 它记住项目首次添加的顺序,
    • 键可以是任何类型,而不仅仅是字符串。
  • A 是比数组数组更有用的结果。但是,如果您确实需要数组数组,则可以调用(对于对数组)或(对于简单的数组数组)。MapArray.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()
);

后期编辑:

对于大多数目的来说,上述内容可能足够有效。但最初的问题是“最有效的”,正如一些人所指出的那样,上述解决方案并非如此。问题主要在于为每个条目实例化一个新数组。我曾以为这会被 JS 解释器优化掉,但似乎可能不是。

有人建议进行编辑来解决这个问题,但它看起来确实更复杂。原始片段已经稍微提高了可读性。如果你真的想这样做,请直接使用for循环!这不是罪过!它需要一两行代码,但它比函数式技术更简单,即使它并不短:

const groupedMap = new Map();
for (const e of initialArray) {
    let thisList = groupedMap.get(e.type);
    if (thisList === undefined) {
        thisList = [];
        groupedMap.set(e.type, thisList);
    }
    thisList.push(e);
}

[编辑:更新为更有效的实现,避免同时执行两者,并针对已经存在的键。.has().get()

评论

6赞 unbob 4/25/2019
我不知道为什么这没有更多的选票。它简洁明了,可读性强(对我来说)并且看起来很高效。它不能在 IE11 上飞行,但改装并不太难(大约)a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map())
3赞 Artem Balianytsia 9/23/2022
因为它实际上是低效的解决方案,因为它在每次 reduce 回调调用时都会实例化一个新数组。
5赞 Benny Powers 12/29/2017 #17

基于之前的答案

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 接受部分格式的返回值(从一个空对象开始),并返回一个由前一个返回值的展开成员组成的对象,以及一个新成员,其键是根据当前迭代者的值计算的,其值是该道具的所有值以及当前值的列表。prop

3赞 Jean-Philippe 4/15/2018 #18

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>

31赞 HoppyKamper 9/13/2018 #19

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

评论

0赞 Stamatis Deliyannis 11/9/2020
显然,我错过了一些东西。为什么我们不能用 MDN 的这个解决方案生成一个数组?如果你尝试用 ,[] 初始化 reducer,你会得到一个空数组。
15赞 SynCap 10/14/2018 #20

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

这是通过某些字段对对象数组的 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

此外,还可以递归对数据进行分组。例如,最初分组依据,然后按字段分组,依此类推。另外吹气 脂肪休息数据。PhaseStep

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

14赞 kevinrodriguez-io 7/16/2019 #21

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

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

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

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

  • 该函数最多需要 4 个参数。这就是为什么要添加第五个参数,以便我们可以使用默认值在参数声明级别为组 (k) 提供廉价的变量声明。(是的,这是巫术)Array.prototype.reduce

  • 如果我们当前的组在上一次迭代中不存在,我们创建一个新的空数组,这将计算到最左边的表达式,换句话说,一个现有的数组或一个空数组,这就是为什么在该表达式后面有一个紧接的数组,因为无论哪种方式,你都会得到一个数组((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[]>);

评论

2赞 Nuwan Dammika 10/30/2019
你愿意解释一下吗,它工作得很好
1赞 kevinrodriguez-io 5/27/2020
@NuwanDammika - 在两个 => 中,你有一个自由的“return” - reduce 函数最多需要 4 个参数。这就是为什么要添加第五个参数,这样我们就可以为组 (k) 提供一个廉价的变量声明。- 如果上一个值没有我们当前的组,我们创建一个新的空组 ((r[k] ||(r[k] = []))这将计算到最左边的表达式,否则是一个数组或一个空数组,这就是为什么在该表达式之后立即推送的原因。- 当有返回值时,逗号运算符将丢弃最左边的值,返回调整后的上一组。
3赞 Dmytro Sokhach 9/3/2020
TS 的最佳语法。最佳答案,当与复杂对象一起使用时。const groups = groupBy(items, (x) => x.groupKey);
0赞 WestCoastProjects 10/10/2020
这太棒了。我是一个 scala 人,感觉宾至如归。井。。除了默认的是什么?
0赞 kevinrodriguez-io 10/13/2020
@javadba导出默认值只是用于 JS 模块的语法,类似于仅导出,默认关键字将允许您像这样导入:import Group from '../path/to/module';
3赞 Pasa89 8/26/2019 #22

我会检查 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());
6赞 Yago Gehres 10/8/2019 #23

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

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

  1. 现在重复项消失了,我们将使用扩展运算符将其转换回数组......

设置 Doc:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set Spread OperatorDoc: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

评论

0赞 Ivan 2/9/2020
我非常喜欢你的回答,这是最短的一个,但我仍然不明白其中的逻辑,尤其是,谁在这里分组?是点差运算符(...)吗?还是“new Set()”?请向我们解释一下......谢谢
1赞 Yago Gehres 2/10/2020
1. 首先,我们通过传递一个数组来创建一个新的集合。由于 Set 仅允许唯一值,因此将删除所有重复项。2.现在重复项消失了,我们将使用扩展运算符将其转换回数组...设置 Doc:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...点差运算符:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
4赞 Aznhar 10/16/2020 #24

我不认为给定的答案是在回答这个问题,我认为以下应该回答第一部分:

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

评论

0赞 CHARFEDDINE Amine 3/9/2022
键 Step Ex 的错误输出:groupBy('Step')
0赞 Aznhar 3/10/2022
是的,我认为你必须先对它进行排序:arr.sort((a, b) => a[key] - b[key]).reduce...我更新了我的答案
0赞 Aznhar 3/10/2022
我的坏:sort((a, b) => a[key].localeCompare(b[key]))
110赞 nkitku 10/23/2020 #25

GroupBy one-liner,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 },
//   ],
// }

groupByToMap

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

评论

1赞 Grant 8/18/2021
||= 被我的巴别塔拒绝了吗?
1赞 loop 8/20/2021
这是最近才标准化的。blog.saeloun.com/2021/06/17/......
2赞 Fung 10/24/2021
我爱我,我简洁的需要多花一点时间来弄清楚神奇的单行本!迄今为止最(主观上)优雅的解决方案。
1赞 Micha Schopman 1/22/2022
非常优雅,尤其是能够以这种方式调整谓词。华丽。
1赞 nkitku 12/4/2022
@vitaly-t 如果你想使用类似数组的对象,你可以使用 Array.from 转换为数组或 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
3赞 Julian 2/9/2021 #26

让我们在重用已经编写的代码(即下划线)的同时完全回答原始问题。如果将 Underscore 的 >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);

给定原始问题中的 和 设置为 ,将获得以下值:arrayOfObjectspropertyNames['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);

鉴于我们之前获得并设置为 ,我们得到提问者想要的:intermediateaggregatePropertyValueresult

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

我们可以把它们放在一个函数中,该函数将 和 作为参数。请注意,它实际上也可以是带有字符串键的普通对象,因为接受任何一个。出于这个原因,我已重命名为 .arrayOfObjectspropertyNamesaggregatePropertyarrayOfObjects_.groupByarrayOfObjectscollection

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

我们可以更进一步,使调用方能够计算每个组中值的任何统计信息。我们可以这样做,还可以使调用方能够将任意属性添加到每个组的摘要中。我们可以在缩短代码的同时完成所有这些工作。我们将参数替换为参数,并将其直接传递给:aggregatePropertyiteratee_.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();
}

实际上,我们将部分责任转移给调用者;她必须提供一个可以传递给 的对象,以便调用将生成一个对象,其中包含她想要添加的聚合属性。例如,我们使用以下表达式获得与之前相同的结果:iteratee_.reduce_.reduceresult

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: +memo.Value + +value.Value
}));

举个稍微复杂的例子,假设我们要计算每个组的最大而不是总和,并且我们想要添加一个属性来列出该组中发生的所有值。这是我们可以做到这一点的一种方法,使用上面的最后一个版本(和 _.union):iterateeValueTasksTaskaggregate

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,他还发布了一个可以处理任意递减函数的答案。我又写了几个 SO 答案,演示了如何通过组合多个 Underscore 函数来实现复杂的事情:

4赞 edin0x 4/7/2021 #27

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

评论

0赞 Mason 4/9/2021
我会对这个版本的性能基准测试感兴趣(在每一轮都有新的数组和解构以创建要设置的值)与另一个仅在需要时创建空数组的基准测试。根据您的代码:gist.github.com/masonlouchart/da141b3af477ff04ccc626f188110f28
0赞 Neek 10/12/2021
需要明确的是,对于偶然发现此的新手来说,这是 Typescript 代码,而原始问题被标记为 javascript,所以这是相当偏离主题的,对吧?
25赞 Kmylo darkstar 8/20/2021 #28

有点晚了,但也许像这样的人。

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

评论

1赞 Praveen Vishnu 10/7/2021
谢谢你的方法有效,我对这个概念有点陌生,你能解释一下initialValue部分它做了什么吗
0赞 Kmylo darkstar 4/11/2022
@PraveenVishnu initialValue 是 reduce 回调的一部分,我只是想显式添加它 developer.mozilla.org/es/docs/Web/JavaScript/Reference/......
0赞 Kmylo darkstar 5/5/2023
使用 ES6 编写此内容的另一种方法是: ''' const groupBy = (arr, key) => { const initialValue = {}; const reducer = (acc, currentObj) => { const groupByKey = currentObj[key]; const currentGroup = acc[groupByKey] ||[];返回 { ...acc, [groupByKey]: [...currentGroup, currentObj], };};返回 arr.reduce(reducer, initialValue);};```
6赞 marko424 1/26/2022 #29

您可以使用本机 JavaScript 数组方法(目前处于第 2 阶段)。group

我认为解决方案比减少或接触第三方库(如 lodash 等)要优雅得多。

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/[email protected]/minified.min.js"></script>

评论

1赞 Константин Ван 5/9/2023
而且.Array.prototype.groupToMap
4赞 Carlos 4/28/2022 #30

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

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');
4赞 XM01 - stands with Palestine 9/29/2023 #31

在 JavaScript 中对对象数组中的元素进行分组的最有效方法是使用内置方法:

Object.groupBy()

const input = [
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { 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 output = Object.groupBy(input, ({ Phase }) => Phase);

console.log(JSON.stringify(output, null, 4));

结果:

{
    "Phase 1": [
        {
            "Phase": "Phase 1",
            "Step": "Step 1",
            "Task": "Task 1",
            "Value": "5"
        }
    ],
    "Phase 2": [
        {
            "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"
        }
    ]
}

注意:该方法现在在 Chrome 117 中受支持,并开始由其他浏览器实现。检查浏览器兼容性Object.groupBy()

core-js 库中 Object.groupBy 的 Polyfill。