
问题描述与目标
在处理异构数据时,我们经常会遇到需要根据某个共同标识符(key)来合并不同数据片段的场景。本教程所面临的问题是:给定一个包含多个json对象的数组,这些对象可能在顶层拥有一个key字段,也可能在一个嵌套的confidential对象中包含key字段(即confidential.key)。我们的目标是将所有具有相同key值的对象合并成一个单一的、更完整的对象。
原始数据示例:
[
{
"key": 111,
"studentInfo": [
{
"details": {
"calculated_fields": null,
"status": false
}
}
]
},
{
"key": 222,
"studentInfo": [
{
"details": {
"calculated_fields": null,
"status": false
}
}
]
},
{
"confidential": {
"data": {
"access_control": {
"private_data": null,
"users": []
}
},
"key": 111
}
},
{
"confidential": {
"data": {
"access_control": {
"private_data": null,
"users": []
}
},
"key": 222
}
}
]期望的合并结果:
[
{
"key": 111,
"studentInfo": [
{
"details": {
"calculated_fields": null,
"status": false
}
}
],
"confidential": {
"data": {
"access_control": {
"private_data": null,
"users": []
}
},
"key": 111
}
},
{
"key": 222,
"studentInfo": [
{
"details": {
"calculated_fields": null,
"status": false
}
}
],
"confidential": {
"data": {
"access_control": {
"private_data": null,
"users": []
}
},
"key": 222
}
}
]可以看到,key为111和222的两个对象都被成功合并,其studentInfo和confidential属性被整合到同一个对象中。
核心合并策略
为了实现上述合并目标,我们可以采用Array.prototype.reduce方法。reduce方法遍历数组中的每个元素,并将其合并到单个累加器值中。在这个场景下,累加器将是一个数组,用于存放最终合并后的对象。
立即学习“Java免费学习笔记(深入)”;
核心思路如下:
- 初始化一个空数组作为reduce的累加器,它将存储最终合并后的对象。
- 遍历输入数组中的每个对象。
-
判断当前对象的键路径:
- 如果当前对象包含顶层key属性,说明它可能是某个合并组的“主”对象,或者至少是该组的第一个出现对象。此时,我们直接将此对象添加到累加器数组中。
- 如果当前对象不包含顶层key,但包含confidential.key属性,则说明它是一个补充信息对象。我们需要根据confidential.key的值,在累加器数组中查找是否存在一个已经存在的、具有相同key值的对象。
-
执行合并操作:
- 如果找到了匹配的对象,则将当前对象的属性合并到已找到的对象中。Object.assign()方法非常适合这种浅层合并。
- 如果没有找到匹配的对象,这通常意味着数据结构可能不符合预期,或者需要将此对象作为新的合并组的起始。在我们的场景中,由于key是唯一的标识符,我们假设所有confidential对象都有对应的顶层key对象。
JavaScript 实现
以下是使用JavaScript实现上述合并逻辑的代码:
const inputData = [
{
key: 111,
studentInfo: [
{
details: {
calculated_fields: null,
status: false,
},
},
],
},
{
key: 222,
studentInfo: [
{
details: {
calculated_fields: null,
status: false,
},
},
],
},
{
confidential: {
data: {
access_control: {
private_data: null,
users: [],
},
},
key: 111,
},
},
{
confidential: {
data: {
access_control: {
private_data: null,
users: [],
},
},
key: 222,
},
},
];
// 使用 reduce 方法进行数据合并
const mergedData = inputData.reduce((accumulator, currentObject) => {
// 如果当前对象包含顶层 'key' 属性
if (currentObject.key) {
// 直接将此对象推入累加器数组
accumulator.push(currentObject);
} else if (currentObject.confidential && currentObject.confidential.key) {
// 如果当前对象包含 'confidential.key' 属性
const targetKey = currentObject.confidential.key;
// 在累加器中查找是否存在与 targetKey 匹配的对象
const existingObject = accumulator.find((obj) => obj.key === targetKey);
// 如果找到了匹配的对象,则进行合并
if (existingObject) {
// 使用 Object.assign 将 currentObject 的属性合并到 existingObject 中
// 注意:这里是浅合并,如果内部对象也需要深度合并,则需要更复杂的逻辑
Object.assign(existingObject, currentObject);
} else {
// 如果没有找到匹配的对象,这可能意味着数据结构不完整或顺序不符合预期
// 根据实际业务需求,可以选择抛出错误、记录日志或将其作为一个新对象加入
// 在本例中,我们假设所有 confidential 对象都有对应的顶层 key 对象
console.warn(`Warning: No matching object found for key ${targetKey} in accumulator.`);
}
}
return accumulator; // 返回更新后的累加器
}, []); // 初始累加器是一个空数组
console.log(JSON.stringify(mergedData, null, 2));代码逻辑详解
-
inputData.reduce((accumulator, currentObject) => { ... }, []);:
- reduce方法被调用在inputData数组上。
- accumulator:累加器,这里是一个空数组[],它将逐步构建最终的合并结果。
- currentObject:inputData数组中当前正在处理的对象。
- []:reduce方法的第二个参数,表示accumulator的初始值。
-
if (currentObject.key):
- 检查当前对象是否直接拥有key属性。
- 如果存在,说明这是一个带有主要标识符的对象(如包含studentInfo的对象),将其直接推入accumulator。这是合并组的起点。
-
else if (currentObject.confidential && currentObject.confidential.key):
- 如果当前对象没有顶层key,则检查它是否包含confidential属性,并且confidential内部是否含有key。
- targetKey = currentObject.confidential.key;:提取出嵌套的key值。
-
const existingObject = accumulator.find((obj) => obj.key === targetKey);:
- 使用find方法在accumulator(目前已合并的对象数组)中查找是否存在一个对象的key与targetKey匹配。
-
if (existingObject) { Object.assign(existingObject, currentObject); }:
- 如果find方法找到了匹配的existingObject,则使用Object.assign()方法将currentObject的所有可枚举属性复制到existingObject上。
- Object.assign执行的是浅合并。这意味着如果currentObject和existingObject有同名的非原始类型属性(如对象或数组),currentObject的属性会完全替换existingObject的属性,而不是合并其内部内容。对于本例,confidential对象替换了existingObject中可能存在的同名属性,这符合预期。
-
return accumulator;:
- 在每次迭代结束时,reduce回调函数必须返回更新后的accumulator,以便在下一次迭代中使用。
注意事项与扩展
-
键的唯一性与存在性:
- 本解决方案依赖于key或confidential.key的准确存在和唯一性。在实际应用中,应考虑这些键可能缺失、为null或重复的情况,并增加相应的错误处理或默认值逻辑。
- 例如,可以使用可选链操作符 currentObject.confidential?.key 来更安全地访问嵌套属性。
-
合并深度:
- Object.assign()执行的是浅合并。如果studentInfo或confidential内部的数组或对象也需要深度合并(例如,合并studentInfo数组中的元素,而不是替换整个studentInfo数组),则需要自定义一个深度合并函数,或者使用如Lodash的_.merge等工具库。
- 对于本教程的示例,浅合并是符合期望的。
-
性能考量:
在reduce回调中,accumulator.find()操作在最坏情况下需要遍历整个accumulator数组。如果inputData非常大,并且accumulator也变得非常大,这可能导致O(N^2)的性能开销。
-
优化建议:对于大规模数据,可以考虑先将数据转换为以key为键的Map或普通对象,以实现O(1)的查找效率,从而将整体复杂度优化到O(N)。
const optimizedMergedData = inputData.reduce((map, currentObject) => { const key = currentObject.key || currentObject.confidential?.key; if (key) { map.set(key, { ...(map.get(key) || {}), ...currentObject }); } return map; }, new Map()); const finalResult = Array.from(optimizedMergedData.values()); console.log(JSON.stringify(finalResult, null, 2));这种优化方案在处理大量数据时会显著提高性能。
-
数据顺序:
- 本解决方案假定带有顶层key的对象会先于或与带有confidential.key的对象一起出现,并且find操作能找到它。如果confidential对象先于其对应的顶层key对象出现,那么find将失败,导致数据丢失或错误。确保数据输入顺序或调整逻辑以处理乱序情况是重要的。本教程的原始数据示例是“顶层key对象在前,confidential对象在后”,因此当前的find逻辑是有效的。
通过以上方法,我们可以灵活高效地处理JSON数据中基于不同键路径的合并需求,生成结构统一、内容完整的数据集。









