JavaScript中从对象数组中提取唯一键值对的教程

本教程旨在解决javascript中从对象数组中移除重复键值对的问题。我们将通过一种高效的算法,利用一个跟踪已出现键值对的辅助数据结构(seen映射),结合array.prototype.reduce方法,遍历输入数组并构建新的对象,确保每个输出对象仅包含在此之前未曾出现的键值对。文章将提供详细的算法解析、typescript实现代码及使用示例,帮助开发者清晰地理解和应用此技术。

JavaScript中提取对象数组的唯一键值对

在处理复杂数据结构时,我们经常需要对数据进行清洗和去重。一个常见的场景是,给定一个包含多个对象的数组,我们希望从这些对象中移除那些键值对(key-value pair)在之前对象中已经出现过的部分,从而得到一个仅包含独特键值对的新对象数组。

问题描述

假设我们有一个对象数组,其中每个对象都包含一组键值对。我们的目标是创建一个新的对象数组,其中每个对象仅包含那些在整个处理过程中首次出现的键值对。如果一个键值对(例如 Param1: "20")已经在之前的对象中出现过,那么它在后续对象中的相同出现将被忽略。

示例输入:

const arr1 = [
  {
    "Param1": "20",
    "Param2": "8",
    "Param3": "11",
    "Param4": "4",
    "Param5": "18",
    "Param6": "20",
    "Param7": "8"
  },
  {
    "Param6": "21",
    "Param7": "8",
    "Param8": "11",
    "Param9": "4",
    "Param10": "18"
  },
  {
    "Param1": "20",
    "Param2": "8",
    "Param3": "10"
  }
];

期望输出:

[
  {
    "Param1": "20",
    "Param2": "8",
    "Param3": "11",
    "Param4": "4",
    "Param5": "18",
    "Param6": "20",
    "Param7": "8"
  },
  {
    "Param6": "21", // 注意:Param6: "20" 已出现,但 Param6: "21" 是新的
    "Param8": "11",
    "Param9": "4",
    "Param10": "18"
  },
  {
    "Param3": "10" // 注意:Param1: "20", Param2: "8" 已出现,但 Param3: "10" 是新的
  }
]

解决方案算法

解决此问题的核心思想是维护一个“已见”状态,记录所有已经处理过的键值对。当遍历数组中的每个对象及其键值对时,我们检查当前键值对是否已在“已见”状态中。

  1. 初始化 seen 映射: 创建一个空的映射(或JavaScript对象),用于存储已遇到的所有 key:value 组合。其结构可以是 { key: { value: boolean } },其中 boolean 值表示该键值对是否已出现。
  2. 初始化 result 数组: 创建一个空数组,用于存储最终处理后的对象。
  3. 遍历输入数组: 逐个处理输入数组中的每个对象。
  4. 构建新对象: 对于当前正在处理的对象,创建一个新的空对象,用于存放其独特的键值对。
  5. 遍历当前对象的键值对: 迭代当前对象的所有 key:value 对。
  6. 检查唯一性并更新 seen:
    • 首先,确保 seen 映射中存在当前 key 的条目(如果不存在,则初始化为一个空对象)。
    • 检查 seen[key][value] 是否为 true。
    • 如果为 true,表示此 key:value 对已经出现过,跳过它。
    • 如果为 false 或 undefined,表示此 key:value 对是首次出现。将其添加到当前正在构建的新对象中,并将 seen[key][value] 设置为 true,以标记其已出现。
  7. 添加至结果: 将构建好的新对象(仅包含独特键值对)添加到 result 数组中。
  8. 返回 result: 遍历完成后,返回 result 数组。

JavaScript/TypeScript 实现

我们可以使用 Array.prototype.reduce 方法来优雅地实现上述算法。reduce 允许我们维护一个累加器,其中可以包含 seen 映射和 result 数组。

type KeyValueMap = Record;
type SeenMap = Record>;

/**
 * 从对象数组中移除重复的键值对。
 * 如果一个键值对(key:value)在之前的对象中已经出现过,则在后续对象中忽略它。
 *
 * @param arr 输入的对象数组,每个对象包含字符串键和字符串值。
 * @returns 包含唯一键值对的新对象数组。
 */
const removeDuplicates = (arr: KeyValueMap[]): KeyValueMap[] => {
    // 使用 reduce 方法来迭代数组并维护一个累加器
    return arr.reduce<{
        seen: SeenMap; // 存储已出现的键值对
        result: KeyValueMap[]; // 存储最终结果
    }>(
        (acc, currentItem) => {
            // 为当前对象创建一个新的对象,只包含唯一的键值对
            const uniqueItem = Object.fromEntries(
                // 遍历当前对象的所有键值对
                Object.entries(currentItem).filter(([key, value]) => {
                    // 确保 seen[key] 存在,如果不存在则初始化为空对象
                    acc.seen[key] = acc.seen[key] ?? {};

                    // 检查当前键值对是否已在 seen 中
                    if (acc.seen[key][value]) {
                        // 如果已存在,则过滤掉此键值对
                        return false;
                    }

                    // 如果是首次出现,则标记为已见
                    acc.seen[key][value] = true;
                    // 并保留此键值对
                    return true;
                }),
            );
            // 将处理后的唯一对象添加到结果数组中
            acc.result.push(uniqueItem);
            return acc;
        },
        { seen: {}, result: [] }, // reduce 的初始累加器值
    ).result; // 最后返回累加器中的 result 数组
};

// 示例用法
const arr1 = [
  {
    "Param1": "20",
    "Param2": "8",
    "Param3": "11",
    "Param4": "4",
    "Param5": "18",
    "Param6": "20",
    "Param7": "8"
  },
  {
    "Param6": "21",
    "Param7": "8",
    "Param8": "11",
    "Param9": "4",
    "Param10": "18"
  },
  {
    "Param1": "20",
    "Param2": "8",
    "Param3": "10"
  }
];

const uniqueArray = removeDuplicates(arr1);
console.log(JSON.stringify(uniqueArray, null, 2));

输出结果:

[
  {
    "Param1": "20",
    "Param2": "8",
    "Param3": "11",
    "Param4": "4",
    "Param5": "18",
    "Param6": "20",
    "Param7": "8"
  },
  {
    "Param6": "21",
    "Param8": "11",
    "Param9": "4",
    "Param10": "18"
  },
  {
    "Param3": "10"
  }
]

注意事项

  1. 值类型限制: 上述实现假设对象的值是原始类型(如字符串、数字),因为它们可以直接用作 seen 映射的键。如果值是对象或数组,则需要进行序列化(如 JSON.stringify)才能在 seen 映射中进行准确比较和存储。
  2. 性能: 该方法的时间复杂度大致为 O(N*K),其中 N 是输入数组的长度,K 是每个对象平均的键值对数量。由于使用了哈希表(JavaScript 对象)作为 seen 映射,查找和插入操作的平均时间复杂度为 O(1)。
  3. 原始数据不变性: 此实现不会修改原始 arr 数组或其中的任何对象,而是返回一个全新的数组和对象,符合函数式编程的原则。
  4. 键值对的顺序: Object.fromEntries 和 Object.entries 默认会保留键的插入顺序(在ES2015及更高版本中),但对于去重逻辑本身,键值对在对象内部的顺序并不影响其唯一性判断。

总结

通过利用 Array.prototype.reduce 结合一个辅助的 seen 映射,我们能够高效且清晰地从对象数组中提取出仅包含唯一键值对的新数组。这种模式在处理需要基于多维条件去重的数据时非常有用,并且可以根据具体需求进行调整,例如,如果去重条件是基于键而不是键值对,则 seen 映射的结构可以进一步简化。理解并掌握这种技术,有助于开发者在JavaScript中更灵活地处理复杂的数据结构。