如何检查两个Map对象是否相等?

43

如何检查两个ES2015 Map对象是否具有相同的(键,值)对集合?

我们假设所有键和值都是原始数据类型。

解决此问题的一种方法是使用map.entries(),从中创建数组,然后按键对该数组进行排序。并且针对另一个Map也执行相同的操作。然后循环遍历这两个数组以比较它们。但是这种方法非常麻烦,还由于排序(性能低效)和创建数组(内存低效)而非常低效。

是否有更好的方法呢?


2
provide code not links - Dustin Poissant
1
在提问之前,你必须自己尝试找到解决方案。这是规则要求的。你必须提供你目前所尝试过的代码示例。 - Dustin Poissant
3
要么实现深度比较,要么使用现有的实现。https://lodash.com/docs#isEqual - Joseph Young
1
没有标准的做法。您的选择是找到一个可以完成它的库或者自己实现。正如我们中的多个人所说,如果您尝试为属性相等性进行深度对象比较,则这是一个棘手的问题,存在循环引用、需要特殊比较手段的特殊对象(例如 Date)等问题。如果仅涉及字符串、数字或布尔值,则不是那么困难。目前您的问题并未说明您只是尝试解决简单情况还是需要嵌套对象的复杂情况。 - jfriend00
5
我认为这个问题没有问题。这并不是一个编码测试;说明一个有效的算法(你已经做到了),然后询问是否有更加经典的算法是可以的。然而,如果你删掉了代码,那些未阅读问题就直接投票的人会觉得这是更好的,这样你就不会再受到干扰了。 - djechlin
显示剩余11条评论
6个回答

60
没有“标准”或“内置”的方法来做到这一点。从概念上讲,您只需要比较两个Map对象是否具有相同的键和每个键的值,并且没有额外的键。
为了使比较尽可能高效,请执行以下优化:
1. 首先检查两个地图上的 .size 属性。如果两个地图的键数不相同,那么它们肯定不相同。 2. 此外,确保它们具有相同数量的键允许您仅迭代一个地图并将其值与另一个地图进行比较。 3. 使用 for (var [key, val] of map1) 迭代器语法迭代键,因此您无需自己构建或排序键数组(应更快且更节省内存)。 4. 最后,如果确保在找到不匹配时立即返回比较,则在它们不相同时会缩短执行时间。
由于 undefined 是 Map 中的合法值,但也是 .get() 在未找到键时返回的值,因此我们必须通过执行额外的 .has() 来注意这一点,如果要比较的值是 undefined。
由于 Map 对象中的键和值本身可以是对象,因此如果您想要深度属性比较对象以确定平等而不仅仅是 JavaScript 默认使用的更简单的 === 用于测试相同对象,则情况会变得更加棘手。或者,如果您只对键和值具有原始数据类型的对象感兴趣,则可以避免这种复杂性。
要测试仅严格的值平等性的函数(检查对象以查看它们是否是相同的物理对象,而不是进行深度属性比较),可以执行下面展示的操作。这使用 ES6 语法有效地迭代映射对象,并在找到不匹配时通过短路并返回 false 尝试提高性能。

"use strict";

function compareMaps(map1, map2) {
    let testVal;
    if (map1.size !== map2.size) {
        return false;
    }
    for (let [key, val] of map1) {
        testVal = map2.get(key);
        // in cases of an undefined value, make sure the key
        // actually exists on the object so there are no false positives
        if (testVal !== val || (testVal === undefined && !map2.has(key))) {
            return false;
        }
    }
    return true;
}

// construct two maps that are initially identical
const o = {"k" : 2}

const m1 = new Map();
m1.set("obj", o);
m1.set("str0", undefined);
m1.set("str1", 1);
m1.set("str2", 2);
m1.set("str3", 3);

const m2 = new Map();
m2.set("str0", undefined);
m2.set("obj", o);
m2.set("str1", 1);
m2.set("str2", 2);
m2.set("str3", 3);

log(compareMaps(m1, m2));

// add an undefined key to m1 and a corresponding other key to m2
// this will pass the .size test and even pass the equality test, but not pass the
// special test for undefined values
m1.set("str-undefined", undefined);
m2.set("str4", 4);
log(compareMaps(m1, m2));

// remove one key from m1 so m2 has an extra key
m1.delete("str-undefined");
log(compareMaps(m1, m2));

// add that same extra key to m1, but give it a different value
m1.set("str4", 5);
log(compareMaps(m1, m2));

function log(args) {
    let str = "";
    for (let i = 0; i < arguments.length; i++) {
        if (typeof arguments[i] === "object") {
            str += JSON.stringify(arguments[i]);
        } else {
            str += arguments[i];
        }
    }
    const div = document.createElement("div");
    div.innerHTML = str;
    const target = log.id ? document.getElementById(log.id) : document.body;
    target.appendChild(div);
}


如果你想进行深度对象比较而不仅仅是比较它们是否为同一物理对象,其中值可以是对象或数组,那么情况会变得更加复杂。

为此,您需要一个深度对象比较方法,该方法考虑了以下所有内容:

  1. 嵌套对象的递归比较
  2. 防止循环引用(可能导致无限循环)
  3. 了解如何比较某些类型的内置对象,例如 Date

由于关于如何进行深度对象比较已经在其他地方写了很多内容(包括在StackOverflow上有很多高票答案),我将假设这不是你问题的主要部分。


你为什么要循环两次?第一次循环不应该就足够了吗,因为你已经检查过大小是否匹配了吗? - Luka
@contrabit - 很好的观点。一开始我没有在那里加上.size比较,所以我需要第二个循环来处理m2中的额外键,但现在我有了.size比较,我认为你是对的。我会删除第二个循环。 - jfriend00
添加了一个检查边缘情况的代码,如果 Map 中的值本身为 undefined,那么第一个版本的代码将把它与缺失的键匹配,因为当键缺失时 .get() 返回的就是这个。 - jfriend00
很好!这显然是内存高效的。我将尝试比较您和我的方法在大量数据上的性能。 - Luka
为特殊的 undefined 值问题添加了另一个测试用例。 - jfriend00
1
@DoAsync - 这是正确的,它从未被设计为比较顺序,因为大多数使用 Map 对象的情况只关心 Map 中有什么,而不关心插入的顺序。如果您想比较顺序,则应以不同的方式进行。请注意,这里提出的问题是“如何检查两个 ES2015 Map 对象是否具有相同的(键,值)对集合?”这就是这个答案的回答。在问题中没有提到插入顺序。 - jfriend00

8
这里有一个检查地图相等性的单行函数:
const mapsAreEqual = (m1, m2) => m1.size === m2.size && Array.from(m1.keys()).every((key) => m1.get(key) === m2.get(key));

2
如果您的Map只有字符串键,那么您可以使用以下方法进行比较:
const mapToObj = (map) => {
  let obj = Object.create(null)
  for (let [k,v] of map) {
    // We don’t escape the key '__proto__'
    // which can cause problems on older engines
    obj[k] = v
  }
  return obj
}

assert.deepEqual(mapToObj(myMap), myExpectedObj)

注意: deepEqual 是许多测试套件的一部分,如果没有,您可以使用lodash/underscore等同函数。任何进行深度比较的函数都可以。

mapToObj 函数由http://exploringjs.com/es6/ch_maps-sets.html提供。


0
这是我的示例,具有提供可选比较函数的能力。
/**
 * The utility function that returns an intersection of two Sets
 *
 * @returns an array of items in common
 */
function intersection<T>(a: Set<T>, b: Set<T>): T[] {
  return Array.from(a).filter(x => b.has(x));
}

/**
 * Compares two Maps
 *
 * @param compare is an optional function for values comparison
 * @returns `true` if they are equal, and `false` otherwise
 */
function compareMaps<T>(a: Map<string, T>, b: Map<string, T>, compare?: (aValue: T, bValue: T) => boolean): boolean {
  const common = intersection(new Set(a.keys()), new Set(b.keys()));
  return a.size === b.size && 
    common.length === a.size && 
    common.every(key => 
      compare?.(a.get(key) as T, b.get(key) as T) ?? a.get(key) === b.get(key));

}


0
以上方法对于 Map<string, object> 不起作用,因为以下行不会正确评估两个对象:

if (testVal !== val || (testVal === undefined && !map2.has(key))) {

下面的版本通过使用 JSON.stringify() 扩展了函数来比较 Map<string, object>
function compareMaps(map1, map2) {
    var testVal;
    if (map1.size !== map2.size) {
        return false;
    }
    for (var [key, val] of map1) {
        testVal = map2.get(key);
        // in cases of an undefined value, make sure the key
        // actually exists on the object so there are no false positives
        if (JSON.stringify(testVal) !== JSON.stringify(val) || (testVal === undefined && !map2.has(key))) {
            return false;
        }
    }
    return true;
}

1
它不需要适用于对象,因为问题陈述了“键和值是原始数据类型”。 - Luka
顺便提一下,如果你想为对象添加约束条件,那么你的代码是不起作用的。尝试将两个不同的函数作为两个对象的属性放置在相同的键上。 - Luka
除此之外,OP表示值是原始类型,JSON.stringify(obj)不是比较对象的规范方法,因为不能保证对象上的属性在JSON中的顺序相同。实际上,对对象进行深度比较需要更多的操作。 - jfriend00

-3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接