【问题标题】:Comparing circular (self-referenced) objects in JavaScript比较 JavaScript 中的循环(自引用)对象
【发布时间】:2019-05-11 01:23:52
【问题描述】:

我正在比较两个对象,它们的值分别为 stringnumberarrayobject。至此没有问题。当我尝试比较自引用对象时,我收到以下错误RangeError: Maximum call stack size exceeded。如果自引用对象被引用到另一个对象的同一级别,则应将它们视为相等。我的问题是如何实现它。这是我的代码:

const equalsComplex = function(value, other) {
  // Get the value type
  const type = Object.prototype.toString.call(value);

  // If the two objects are not the same type, return false
  if (type !== Object.prototype.toString.call(other)) return false;

  // If items are not an object or array, return false
  if (['[object Array]', '[object Object]'].indexOf(type) < 0) return false;

  // Compare the length of the length of the two items
  const valueLen =
    type === '[object Array]' ? value.length : Object.keys(value).length;
  const otherLen =
    type === '[object Array]' ? other.length : Object.keys(other).length;
  if (valueLen !== otherLen) return false;

  // Compare two items
  const compare = function(item1, item2) {
    // Get the object type
    const itemType = Object.prototype.toString.call(item1);

    // If an object or array, compare recursively
    if (['[object Array]', '[object Object]'].indexOf(itemType) >= 0) {
      if (!equalsComplex(item1, item2)) return false;
    }

    // Otherwise, do a simple comparison
    else {
      // If the two items are not the same type, return false
      if (itemType !== Object.prototype.toString.call(item2)) return false;

      // Else if it's a function, convert to a string and compare
      // Otherwise, just compare
      if (itemType === '[object Function]') {
        if (item1.toString() !== item2.toString()) return false;
      } else {
        if (item1 !== item2) return false;
      }
    }
  };

  // Compare properties
  if (type === '[object Array]') {
    for (let i = 0; i < valueLen; i++) {
      if (compare(value[i], other[i]) === false) return false;
    }
  } else {
    for (let key in value) {
      if (value.hasOwnProperty(key)) {
        if (compare(value[key], other[key]) === false) return false;
      }
    }
  }

  // If nothing failed, return true
  return true;
};
const r = { a: 1 };
r.b = r;
const d = { a: 1 };
d.b = d;

console.log(
  equalsComplex(
    {
      a: 2,
      b: '2',
      c: false,
      g: [
        { a: { j: undefined } },
        { a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
        r
      ]
    },
    {
      a: 2,
      b: '2',
      c: false,
      g: [
        { a: { j: undefined } },
        { a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
        r
      ]
    }
  )
);

【问题讨论】:

  • 您可以发送一组您已经处理/验证的值吗?
  • 这将允许您停止无限递归,但您能否判断循环对象在两个参数中是否位于同一位置?
  • @Icepickle 我不太明白
  • this answer 声称 underscore.js 中的 _.isEqual() 函数将执行此操作。但是,文档并没有这么说。
  • @Barmar 我还没读过那么多问题????我只是检查递归部分

标签: javascript


【解决方案1】:

开始之前

您是否有理由不使用像 deep-equal 这样的现有库?有时使用已经为您编写的代码比自己编写更容易

现在修复了代码中的一些简单问题

对于初学者来说,使用Object.prototype.toString 来确定类型感觉就像是一种黑客攻击,如果不同的浏览器以不同的方式实现toString 方法,将来可能会出现错误。如果有人知道 toString 方法的返回值是否在 ECMAScript 规范中明确定义,请插话。否则,我会避免这种 hack,因为 JavaScript 提供了一个完美的替代方案:typeofhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof

有趣的是,typeof value 将返回相同的对象 数组,因为就 ECMAScript 而言,数组是对象的子类。因此,您以后对[Object object][Object Array] 的比较可以简化为只检查object 的类型

一旦您开始使用typeof value 而不是Object.prototype.toString.apply(value),您将需要一种方法来区分对象和数组以进行比较。为此,您可以使用Array.isArray

问题的实质

现在关于自我引用,您所指的问题是一个循环。一个简单的循环是:

var a = {};
a.foo = a;

这会创建循环:a.foo.foo.foo.foo.foo.... == a

有一个很好的方法可以检查两个引用是否指向 JavaScript 中的同一个对象,这有助于确定何时相等 true,但它不会在相等性为 false 的情况下提供帮助。要检查两个引用是否指向同一个对象,只需使用 == 运算符!这返回true 是对象指向内存中完全相同的实例。例如:

var a = {foo: "bar"}
var b = {foo: "bar"}
var c = a;

a == b; // false
a == c; // true
b == c; // false

因此,您可以通过检查 item1 == item2 来轻松查看两个引用是否相同

但是当它们相等时,您仍然会执行complexCompare,这将深入每个自引用,并且将具有相同的堆栈溢出.要解决此问题,您需要一种检测周期的方法。与深度平等一样,there are libraries for this,但出于智力原因,我们将看看是否可以重新创建它们。

为此,我们需要记住我们见过的所有其他对象,并在递归时与它们进行比较。一个简单的解决方案可能如下所示:

var objectsWeveSeen = [];

function decycle(obj) {
    for (var key in obj) {
        if (typeof obj[key] == "object") {
            for (var i = 0; i < objectsWeveSeen.length; i++) {
                if (objectsWeveSeen[i] == obj[key]) {
                    obj[key] = "CYCLE! -- originally seen at index " + i;
                }
            }
            objectsWeveSeen.push(obj[key]);
        }
    }
}

(注意:这个 decycle 函数是破坏性的。它修改了原始对象。此外,这个 decycle 函数不是递归的,所以它实际上很烂。但它至少给了你一个大致的想法,你可以尝试编写自己的,或者看看别人是怎么做的)

然后我们可以像这样向它传递一个对象:

var a = {foo: {}};
a.baz = a.foo;
console.log(decycle(a));
// Outputs: {foo: {}, baz: "CYCLE! -- originally seen at index 0"}

由于这个对象没有循环,您现在可以对其进行复杂的比较:

complexCompare(decycle(a));

当然,还有一些边缘情况需要考虑。如果两个 Date 对象引用相同的时间但具有不同的时区,它们是否等效? null 是否等于 null?而且我的简单 decycle 算法无法解释对 root 对象的引用,它只记住所有 keys它已经看到了(尽管如果您考虑一下,这应该很容易添加)

一个不太完美但可行的解决方案

我没有写出完美的 deep-equals 实现有两个原因:

  1. 我觉得写代码是最好的学习方式,而不是从别人那里复制粘贴
  2. 我确定有一些我没有考虑到的极端情况(这就是您应该使用像 Lodash 这样久经考验的库而不是编写自己的代码的原因)并且承认这是一个不完整的解决方案,而不是把它卖掉,你会被鼓励去找写更完整答案的人
function complexCompare(value, other) {
    var objectsWeveSeen = [];
    function nonDestructiveDecycle(obj) {
        var newObj = {};
        for (var key in obj) {
            newObj[key] = obj[key];
            if (typeof obj[key] == "object") {
                for (var i = 0; i < objectsWeveSeen.length; i++) {
                    if (objectsWeveSeen[i] == obj[key]) {
                        newObj[key] = "CYCLE! -- originally seen at index " + i;
                        break;
                    }
                }
                objectsWeveSeen.push(obj[key]);
            }
        }
        return newObj;
    }

    var type = typeof value;
    if (type !== typeof other) return false;

    if (type !== "object") return value === other;

    if (Array.isArray(value)) {
        if (!Array.isArray(other)) return false;

        if (value.length !== other.length) return false;

        for (var i = 0; i < value.length; i++) {
            if (!complexCompare(value[i], other[i])) return false;
        }

        return true;
    }

    // TODO: Handle other "object" types, like Date

    // Now we're dealing with JavaScript Objects...
    var decycledValue = nonDestructiveDecycle(value);
    var decycleOther = nonDestructiveDecycle(other);

    for (var key in value) {
        if (!complexCompare(decycledValue[key], decycleOther[key])) return false;
    }

    return true;
}

更新

回应cmets:

=====

== 在两个变量之间执行“松散”比较。例如,3 == "3" 将返回 true。 === 在两个变量之间执行“严格”比较。所以3 === "3" 将返回 false。在我们的例子中,你可以使用你喜欢的任何一个,结果应该没有区别,因为:

  • typeof 总是返回一个字符串。因此typeof x == typeof ytypeof x === typeof y 完全相同
  • 如果您在比较它们的值之前检查两个变量的类型是否相同,那么您绝不应该遇到===== 返回不同结果的极端情况之一。例如,0 == falsetypeof 0 != typeof false0 是“数字”,false 是“布尔值”)

我坚持使用== 作为我的示例,因为我觉得避免两者之间的任何混淆会更熟悉

[]Set

我查看了使用Set 重写decycle 并很快遇到了问题。您可以使用Set 来检测是否存在 循环,但您不能轻易地使用它来检测两个循环是否相同。请注意,在我的decycle 方法中,我用字符串CYCLE! -- originally seen at index X 替换了一个循环。这个“在索引 X”的原因是因为它告诉您 哪个 对象被引用。我们不只是拥有“我们以前见过的一些物体”,而是拥有“我们以前见过的那个物体”。现在,如果两个对象引用同一个对象,我们可以检测到(因为字符串将相等,具有相同的索引)。如果两个对象引用了不同的,我们也会检测到(因为字符串不相等)

但是,我的解决方案存在问题。考虑以下几点:

var a = {};
a.foo = a;

var b = {};
b.foo = b;

var c = {};
c.foo = a;

在这种情况下,我的代码会声明 ac 相等(因为它们都引用同一个对象)但 ab 不是(因为即使它们具有相同的值、相同的模式和相同的结构——它们引用不同的对象)

更好的解决方案可能是将“索引”(表示我们找到对象的顺序的数字)替换为“路径”(表示如何到达对象的字符串)

var objectsWeveSeen = []

function nonDestructiveRecursiveDecycle(obj, path) {
    var newObj = {};
    for (var key in obj) {
        var newPath = path + "." + key;
        newObj[key] = obj[key];
        if (typeof obj[key] == "object") {
            for (var i = 0; i < objectsWeveSeen.length; i++) {
                if (objectsWeveSeen[i].obj == obj[key]) {
                    newObj[key] = "$ref:" + objectsWeveSeen[i].path;
                    break;
                }
            }
            if (typeof newObj[key] != "string") {
                objectsWeveSeen.push({obj: obj[key], path: newPath});
                newObj[key] = nonDestructiveRecursiveDecycle(obj[key], newPath);
            }
        }
    }
    return newObj;
}

var decycledValue = nonDestructiveRecursiveDecycle(value, "@root");

【讨论】:

  • 您不能在答案中包含SetArray.isArray 吗? :) 我喜欢你回答的深度,但我也愿意使用=====。我认为var b = {foo: bar] 有一个小错字
  • 我之前实际上没有使用过Set,所以如果不做一些谷歌搜索我就无法包含它:) 虽然Array.isArray 绝对值得添加,因为一旦你开始使用typeof要检查object,您需要一种在比较数组和对象之前区分它们的方法。我一定会补充的。
  • @stevendesu 回答您关于使用现有库(如 deep-equal)的问题。我需要用纯 JS 实现它
  • objectsWeveSeen = new Set;,然后是 objectsWeveSeen.add(someObj)objectsWeveSeen.has(someObj)。这将大大降低时间复杂度。否则很好的答案。
  • @stevendesu Object.prototype.toString 这个 hack 是从 MDN 文档 - Using toString() to detect object class 部分借来的。您的答案是一个可行的解决方案。但说实话,我必须消化它才能完全理解。感谢您的努力!
【解决方案2】:

我喜欢@stevendesu 的回复。他很好地解决了循环结构的问题。我使用您的代码编写了一个解决方案,可能也会有所帮助。

const equalsComplex = function(value, other, valueRefs, otherRefs) {
  valueRefs = valueRefs || [];
  otherRefs = otherRefs || [];

  // Get the value type
  const type = Object.prototype.toString.call(value);

  // If the two objects are not the same type, return false
  if (type !== Object.prototype.toString.call(other)) return false;

  // If items are not an object or array, return false
  if (['[object Array]', '[object Object]'].indexOf(type) < 0) return false;

  // We know that the items are objects or arrays, so let's check if we've seen this reference before.
  // If so, it's a circular reference so we know that the branches match. If both circular references
  // are in the same index of the list then they are equal.
  valueRefIndex = valueRefs.indexOf(value);
  otherRefIndex = otherRefs.indexOf(other);
  if (valueRefIndex == otherRefIndex && valueRefIndex >= 0) return true;
  // Add the references into the list
  valueRefs.push(value);
  otherRefs.push(other);

  // Compare the length of the length of the two items
  const valueLen =
    type === '[object Array]' ? value.length : Object.keys(value).length;
  const otherLen =
    type === '[object Array]' ? other.length : Object.keys(other).length;
  if (valueLen !== otherLen) return false;

  // Compare two items
  const compare = function(item1, item2) {
    // Get the object type
    const itemType = Object.prototype.toString.call(item1);

    // If an object or array, compare recursively
    if (['[object Array]', '[object Object]'].indexOf(itemType) >= 0) {
      if (!equalsComplex(item1, item2, valueRefs.slice(), otherRefs.slice())) return false;
    }

    // Otherwise, do a simple comparison
    else {
      // If the two items are not the same type, return false
      if (itemType !== Object.prototype.toString.call(item2)) return false;

      // Else if it's a function, convert to a string and compare
      // Otherwise, just compare
      if (itemType === '[object Function]') {
        if (item1.toString() !== item2.toString()) return false;
      } else {
        if (item1 !== item2) return false;
      }
    }
  };

  // Compare properties
  if (type === '[object Array]') {
    for (let i = 0; i < valueLen; i++) {
      if (compare(value[i], other[i]) === false) return false;
    }
  } else {
    for (let key in value) {
      if (value.hasOwnProperty(key)) {
        if (compare(value[key], other[key]) === false) return false;
      }
    }
  }

  // If nothing failed, return true
  return true;
};
const r = { a: 1 };
r.b = {c: r};
const d = { a: 1 };
d.b = {c: d};

console.log(
  equalsComplex(
    {
      a: 2,
      b: '2',
      c: false,
      g: [
        { a: { j: undefined } },
        { a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
        r
      ]
    },
    {
      a: 2,
      b: '2',
      c: false,
      g: [
        { a: { j: undefined } },
        { a: 2, b: '2', c: false, g: [{ a: { j: undefined } }] },
        d
      ]
    }
  )
);

基本上,您会跟踪迄今为止在每个分支中看到的对对象和数组的引用(slice() 方法制作引用数组的浅表副本)。然后,每次你看到一个对象或一个数组时,你都会检查你的引用历史,看看它是否是循环引用。如果是这样,请确保两个循环引用都指向历史的同一部分(这很重要,因为两个循环引用可能指向对象结构中的不同位置)。

我建议为此使用库,因为我没有深入测试我的代码,但有一个简单的解决方案适合您。

【讨论】:

  • 感谢您的贡献,这对您有很大帮助!
猜你喜欢
  • 2011-11-14
  • 2017-08-04
  • 2014-02-08
  • 1970-01-01
  • 2014-07-28
  • 1970-01-01
  • 2015-09-19
  • 2021-07-25
  • 1970-01-01
相关资源
最近更新 更多