使用 javascript 的嵌套分组 (ES5)

Nested grouping with javascript (ES5)

我有一个对象数组如下:

    [
        {"id":1,"lib":"A","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
        {"id":2,"lib":"B","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
        ...
        {"id":110,"lib":"XXX","categoryID":90,"categoryTitle":"Cat90","moduleID":"4","moduleTitle":"Module 4"}
    ]

我想将此数组按 (moduleID,moduleTitle) 分组,然后按 (categoryID,categoryTitle).

这是我试过的:

function groupBy(data, id, text) {

    return data.reduce(function (rv, x) { 
        var el = rv.find(function(r){
            return r && r.id === x[id];
        });
        if (el) { 
            el.children.push(x);
        } else { 
            rv.push({ id: x[id], text: x[text], children: [x] }); 
        } 
        return rv; 
    }, []);

}

var result = groupBy(response, "moduleID", "moduleTitle");

result.forEach(function(el){
    el.children = groupBy(el.children, "categoryID", "categoryTitle");
});

上面的代码按预期工作,但如您所见,在第一次分组后,我不得不再次迭代按 moduleId 分组的数组,以便按 categoryId.

如何修改此代码,以便我只能在数组上调用一次 groupBy 函数?

编辑:

抱歉,这可能会迟到,但我希望通过使用 ES5 来完成,不要使用 Shim,也不要使用 Polyfill。

这是一种可能的(虽然可能有点高级)方法:

class DefaultMap extends Map {
    constructor(factory, iter) {
        super(iter || []);
        this.factory = factory;
    }

    get(key) {
        if (!this.has(key))
            this.set(key, this.factory());
        return super.get(key);
    }
}

基本上,是 Map 在缺少值时调用工厂函数。现在,有趣的部分:

let grouper = new DefaultMap(() => new DefaultMap(Array));

for (let item of yourArray) {
    let firstKey = item.whatever;
    let secondKey = item.somethingElse;
    grouper.get(firstKey).get(secondKey).push(item);
}

对于每个 firstKey,这会在 grouper 中创建一个 Map,并且这些映射的值是按第二个键分组的数组。

您的问题中更有趣的部分是您使用的是复合键,这在 JS 中非常棘手,因为它(几乎)不提供不可变的数据结构。考虑:

items = [
    {a: 'one', b: 1},
    {a: 'one', b: 1},
    {a: 'one', b: 2},
    {a: 'two', b: 2},
]

let grouper = new DefaultMap(Array);

for (let x of items) {
    let key = [x.a, x.b];      // wrong!
    grouper.get(key).push(x);
}

因此,我们天真地通过复合键对对象进行分组,并期望在我们的石斑鱼中看到 ['one', 1] 下的两个对象(为了示例,这是一个级别)。当然,那是行不通的,因为每个 key 都是一个新创建的数组,并且对于 Map 或任何其他键控存储,它们都是不同的。

一个可能的解决方案是为每个键创建一个不可变结构。一个明显的选择是使用 Symbol,例如

let tuple = (...args) => Symbol.for(JSON.stringify(args))

然后

for (let x of items) {
    let key = tuple(x.a, x.b);      // works
    grouper.get(key).push(x);
}

你可以这样做:

  const exit = Symbol("exit");

  function groupBy(arr, ...props){
    const root = {};
    for(const el of arr){
      const obj = props.map(key => el[key])
          .reduce((obj, key) => obj[key] || (obj[key] = {}), root);
      (obj[exit] || (obj[exit] = [])).push(el);
    }
 }

因此您可以像这样访问它:

  const grouped = groupBy(response, "moduleID", "moduleTitle");

 console.log( grouped[2]["workflow"][exit] );

你可能会省略那个退出符号,但是把嵌套树和数组混合在一起感觉有点不对。

您可以通过使用数组进行分组来扩展您的函数 id/names。

function groupBy(data, groups) {
    return data.reduce(function (rv, x) {
        groups.reduce(function (level, key) {
            var el;

            level.some(function (r) {
                if (r && r.id === x[key[0]]) {
                    el = r;
                    return true;
                }
            });
            if (!el) {
                el = { id: x[key[0]], text: x[key[1]], children: [] };
                level.push(el);
            }
            return el.children;
        }, rv).push({ id: x.id, text: x.lib });
        return rv;
    }, []);
}

var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }],
    result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

路径为 id.

的版本

function groupBy(data, groups) {
    return data.reduce(function (rv, x) {
        var path = [];
        var last = groups.reduce(function (level, key, i) {
            path.length = i;
            path[i] = key[0].slice(0, -2).toUpperCase() + ':' + x[key[0]];

            var id = path.join(';'),
                el = level.find(function (r) {
                    return r && r.id === id;
                });

            if (!el) {
                el = { id: path.join(';'), text: x[key[1]], children: [] };
                level.push(el);
            }
            return el.children;
        }, rv);

        last.push({ id: path.concat('NODE:' + x.id).join(';') });
        return rv;
    }, []);
}

var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }];

    var result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }