对象数组合并四法

在日常开发工作中总结的一个实用方法:当两个数组的长度不一致时,按索引直接合并就不再可靠了,需要基于指定的属性进行匹配连接。常见了有四种,类似于SQL中的连接操作。

假设我们有以下两个数组:

1
2
3
4
5
6
7
8
9
10
const arr1 = [
{ id: 1, name: "aaa" },
{ id: 2, name: "bbb" },
{ id: 4, name: "ddd" }, // 注意缺少 id = 3
];
const arr2 = [
{ id: 1, age: 20 },
{ id: 3, age: 30 }, // arr1 中没有
{ id: 4, age: 40 },
];
  1. 左连接(Left join,以 arr1 为主),保留 arr1 中所有对象,arr2 有则合并,无则对应字段为 undefined,或者使用默认值

    !注意:缺失值给 null 或者 默认值时,对象展开顺序是关键,默认值属性要放在展开对象最前面,否则会覆盖所有值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 高效:先建一个字典,再查找
const lookup = new Map(arr2.map((item2) => [item2.id, item2]));
const leftMerge = arr1.map((item1) => ({
...item1,
...lookup.get(item1.id),
// arr2 中找不到时,展开 ...undefined 相当于没加属性;{ ...undefined }
// 在 ES2018 规范中不会报错,也不会添加任何属性
}));
const leftSafeMerge = arr1.map((item1) => {
const item2 = lookup.get(item1.id) || {};
return { ...item1, ...item2 };
// 如果希望给缺失的属性给 null,注意 age:null 需放在最前面,否则会覆盖所有值
// return {age: null, ...item1, ...item2}
// 如果希望给缺失的属性给默认值,注意 age: 50 需放在最前面,否则会覆盖所有值
// return { age: 50, ...item1, ...item2},
});

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// leftMerge && leftSafeMerge
[
{ id: 1, name: "aaa", age: 20 },
{ id: 2, name: "bbb" },
{ id: 4, name: "ddd", age: 40 },
];
// 给缺失的属性 age 给 null
[
{ id: 1, name: "aaa", age: 20 },
{ age: null, id: 2, name: "bbb" },
{ id: 4, name: "ddd", age: 40 },
];
// 给缺失的属性 age 给默认值 50
[
{ id: 1, name: "aaa", age: 20 },
{ age: 50, id: 2, name: "bbb" },
{ id: 4, name: "ddd", age: 40 },
];
  1. 右连接(Right join, 以 arr2 为主),与左连接相反,以 arr2 为基准,不匹配的属性从 arr1 获取。(左右连接中,连接哪个数组,哪个数组就作为 Map)
1
2
3
4
5
6
7
8
9
10
11
12
13
const lookup = new Map(arr1.map((item1) => [item1.id, item1]));
const rightMerge = arr2.map((item2) => ({
...lookup.get(item2.id), // arr1 中找不到则展开,找不到展开 undefined
...item2,
}));
const rightSafeMerge = arr2.map((item2) => {
const item1 = lookup.get(item2.id) || {};
return { ...item1, ...item2 };
// 如果希望给缺失的属性给 null, 注意 name:null 需放在最前面,否则会覆盖所有值
// return { name: null, ...item1, ...item2}
// 如果希望给缺失的属性给默认值, 注意 name:'bbb'需放在最前面,否则会覆盖所有值
// return { name: 'bbb', ...item1, ...item2}
});

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// rightMerge && rightSafeMerge
[
{ id: 1, name: "aaa", age: 20 },
{ id: 3, age: 30 },
{ id: 4, name: "ddd", age: 40 },
];
// 给缺失的属性 name 给 null
[
{ id: 1, name: "aaa", age: 20 },
{ name: null, id: 3, age: 30 },
{ id: 4, name: "ddd", age: 40 },
];

// 给缺失的属性 name 给 "bbb"
[
{ id: 1, name: "aaa", age: 20 },
{ name: "bbb", id: 3, age: 30 },
{ id: 4, name: "ddd", age: 40 },
];
  1. 内连接(Inner join,只保留两边都有的,取交集)
    只返回属性值在两个数组中都存在的对象,还是要先根据共有的属性建立映射,通过过滤来实现
1
2
3
4
const lookup = new Map(arr2.map((item2) => [item2.id, item2]));
const innerMerged = arr1
.filter((item1) => lookup.has(item1.id))
.map((item1) => ({ ...item1, ...lookup.get(item1.id) }));

结果:

1
2
3
4
[
{ id: 1, name: "aaa", age: 20 },
{ id: 4, name: "ddd", age: 40 },
];

4、全外连接(Full outer join,两边的都要,取并集)
保留两个数组所有的 id, 某一方缺失则为 undefined

1
2
3
4
5
6
7
8
const map1 = new Map(arr1.map((item1) => [item1.id, item1]));
const map2 = new Map(arr2.map((item2) => [item2.id, item2]));
const allIds = new Set([...map1.keys(), ...map2.keys()]);
// Set(4) { 1, 2, 4, 3 }
const fullMerged = Array.from(allIds, (id) => ({
...map1.get(id),
...map2.get(id),
}));

结果:

1
2
3
4
5
6
[
{ id: 1, name: "aaa", age: 20 },
{ id: 2, name: "bbb" }, // age undefined
{ id: 4, name: "ddd", age: 40 },
{ id: 3, age: 30 }, // name undefined
];

核心就是 Map + map,看谁调用 map, 谁就是最终的老大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1.左连接:以 arr1 为主,map 遍历 arr1
const lookup = new Map(arr2.map((v) => [v.id, v]));
const left = arr1.map((v1) => ({ ...v1, ...lookup.get(v1.id) }));

// 2.右连接:以 arr2 为主,map 遍历 arr2
const lookup = new Map(arr1.map((v) => [v.id, v]));
const right = arr2.map((v2) => ({ ...v2, ...lookup.get(v2.id) }));

// 3.内连接: 左连接 + 过滤空值
const inner = arr1
.filter((v1) => lookup.has(v1.id))
.map((v1) => ({ ...v1, ...lookup.get(v1.id) }));

//4.外连接: 合并所有ID,遍历这个集合
const allIds = new Set([...arr1, ...arr2].map((v) => v.id));
const full = [...allIds].map((id) => ({
...map1.get(id),
...map2.get(id),
}));

利用 Map 将查找时间复杂度从 O(n) 降到 O(1), 更适合数万级数据的性能。