JS数组对象合并

Bob_pang
  • 22
var arr = [
   {name:'lilei', age:18, eggs:10},
   {name:'haozi', age:20, eggs:7},
   {name:'lilei', age:18, eggs:5},
];

比如这个我想遍历一下这个数组,然后根据对象属性找到名字和age都相同的,然后把拥有的eggs数量相加,怎么算呢?

回复
阅读 1.3k
5 个回答
然后去远足
  • 33.3k
✓ 已被采纳

本来这种非常基础的代码题我都是直接跳过的,因为我觉得任何一个学了基础的语法的人稍微动动脑子就不可能不会。但闲着也是闲着就答一下吧。

我不会像很多人那样直接甩一个最终答案上来,那样你下次遇到同样的问题该不会还是不会。我们循序渐进一下。


首先是最基本的用双重 for 循环。(为什么我不愿意答也正是因为此,用 groupBy、reduce 之类的“高级”写法写不出来,难道最基本的 for 循环也不会吗?那到底学 JS 学啥了?)

let arr = [ /* 初始值略 */ ];
let res = [];
for (let i = 0; i < arr.length; i++) {
    let merged = false;
    for (let j = 0; j < res.length; j++) {
        if (res[j].name === arr[i].name && res[j].age === arr[i].age) {
            res[j].eggs += arr[i].eggs;
            merged = true;
            break;
        }
    }

    if (!merged) {
        res.push(arr[i]);
    }
}

console.log(res);

代码不解释。


然后想一想,JS 中有没有内置的方法可以直接帮我们找到数组中满足条件的那一个对象?这样就不用写双重 for 循环去匹配了。

当然有!Array.prototype.findArray.prototype.findIndexArray.prototype.filter 都可以。

那我们以 find 为例:

let arr = [ /* 初始值略 */ ];
let res = [];
arr.forEach(m => {
    let obj = res.find(n => n.name === m.name && n.age === m.age);
    if (obj) {
        obj.eggs += m.eggs;
    } else {
        res.push(m);
    }
});

console.log(res);

代码简洁了不少。但还是得一个个 push 进去。有没有办法根据 arr 一下子就能返回结果、就像 Array.prototype.map 那样?

自然想到用 Array.prototype.reduce

let arr = [ /* 初始值略 */ ];
let res = arr.reduce((acc, cur) => {
    let obj = acc.find(e => e.name === cur.name && e.age === cur.age);
    if (obj) {
        obj.eggs += cur.eggs;
    } else {
        acc.push(cur);
    }
    return acc;
}, []);
console.log(res);

【以下是引申问题】

最后,我们还可以继续优化。这里的 reduce+find,本质上还是双重循环,只是没让你显式地写出来 for 而已。那么有没有只循环一次的办法呢?

这个问题留给你自己思考。

可以分几步实现:

分组
const grouped = Object.values(
    arr.groupBy(({name, age}) => JSON.stringify([name, age])),
);

这里用了Stage 3的Array.prototype.groupBy,可以自己实现,或者使用Polyfill。

对分组完的数据做合并
const result = grouped.map(
    items => items.reduce(
        (accumulator, item) => ({
            ...accumulator,
            ...item,
            eggs: accumulator.eggs + item.eggs,
        }),
    ),
);

现在我们可以将他们合并到一起,并参数化支持多个数值的匹配:

/**
 * @template T
 * @param {T[]} target
 * @param {keyof T} key - The key of value to reduced
 * @param {(keyof T)[]} keys - Keys to be grouped by
 * @returns {T[]}
 */
function groupAndSumWithKey (target, key, keys) {
    return Object
        .values(
            target.groupBy(
                item => JSON.stringify(
                    keys.map(key => item[key]),
                ),
            ),
        )
        .map(
            items => items.reduce(
                (accumulator, item) => ({
                    ...accumulator,
                    ...item,
                    [key]: (accumulator[key] ?? 0) + (item[key] ?? 0),
                }),
            ),
        );
}

于是我们实现了自定义累加的key,并且可以自定义比较的key,甚至支持累计的key和比较的key相同。

接下来,我们再进一扩展,支持自定义比较方法,和自定义迭代方法:

/**
 * @template T
 * @param {T[]} target
 * @param {(element: T, index: number, target: T[]) => unknown} groupCallbackFn
 * @param {(previousValue: T, currentValue: T, currentIndex: number, target: T[]) => T} reduceCallBackFn
 * @returns {T[]}
 */
function groupAndReduce (target, groupCallbackFn, reduceCallBackFn) {
    return Array.from(
        target.groupByToMap(groupCallbackFn).values(),
        items => items.reduce(reduceCallBackFn),
    );
}

/**
 * @template T
 * @param {T} target
 * @param {(keyof T)[]} keys
 * @returns {string}
 */
function generateIdByKeys (target, ...keys) {
    return JSON.stringify(keys.map(key => target[key]));
}

/**
 * @param {typeof arr[0]} item
 * @returns {string}
 */
function groupFn (item) {
    return generateIdByKeys(item, "name", "age");
}

/**
 * @param {typeof arr[0]} accumulator
 * @param {typeof arr[0]} item
 * @returns {typeof arr[0]}
 */
function reduceFn (accumulator, item) {
    return {
        ...accumulator,
        ...item,
        eggs: accumulator.eggs + item.eggs,
    };
}

groupAndReduce(arr, groupFn, reduceFn);

如果不想用Array.prototype.groupByArray.prototype.groupByToMap也是可以的。
因为我们不需要追溯前项数据,所以可以把先分组再累加的步骤合并成边分组边累加。groupAndReduce可以改成:

/**
 * @template T
 * @param {T[]} target
 * @param {(element: T, index: number, target: T[]) => unknown} groupCallbackFn
 * @param {(previousValue: T, currentValue: T, currentIndex: number, target: T[]) => T} reduceCallBackFn
 * @returns {T[]}
 */
function groupAndReduce (target, groupCallbackFn, reduceCallBackFn) {
    /** @type {Map<unknown, T>} */
    const groupMap = new Map();

    for (let i = 0, item = target[i]; item; item = target[i += 1]) {
        const id = groupCallbackFn(item, i, target);
        if (groupMap.has(id))
            groupMap.set(id, reduceCallBackFn(groupMap.get(id), item, i, target));
        else
            groupMap.set(id, item);
    }

    return [...groupMap.values()];
}
var newArr = arr.reduce(
    (prev, curr) => (
        !prev.some((item) => item.name === curr.name && item.age === curr.age && (item.eggs += curr.eggs)) &&
            prev.push(curr),
        prev
    ),
    []
);
console.log(newArr);
function sortSum (arr=[]) {
    return arr.reduce((pre, cur) => {
        let key = `${cur.name}-${cur.age}`;
        if (pre[key]!==undefined) {
            pre[key] += cur.eggs;
        } else {
            pre[key] = cur.eggs;
        }
        return pre;
    }, {});
}

返回结果:{lilei-18: 15, haozi-20: 7}

未觉雨声
  • 1.3k

最直接的就是用一个两层的对象做记录,第一层是 name 第二层是 age

const record = {}

arr.forEach(({ name, age, eggs }) => {
  if (!record[name]) {
    record[name] = {}
  }

  const nameRecord = record[name]

  if (!nameRecord[age]) {
    nameRecord[age] = 0
  }

  nameRecord[age] += eggs
})

record.lilei['18'] // 15

这其实就是 groupBy 算法,这里直接大白话写的,你可以自己思考一下怎么支持任意 N 层的 groupBy~

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏