比较JavaScript中的循环(自引用)对象

时间:2019-05-08 19:55:24

标签: javascript

我正在比较两个对象,它们包含值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
      ]
    }
  )
);

2 个答案:

答案 0 :(得分:3)

开始之前

您是否没有使用deep-equal之类的现有库是有原因的吗?有时候,使用已经为您编写的代码比亲自编写要容易

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

对于初学者来说,利用Object.prototype.toString来确定类型就像是黑客一样,并且如果不同的浏览器以不同的方式实现toString方法,将来可能会冒错误的风险。如果有人知道ECMAScript规范中是否明确定义了toString方法的返回值,请发出提示。否则,我将避免这种黑客攻击,因为JavaScript提供了一种完美的选择:typeof {{3 }}

有趣的是,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,它将深入到每个自我参考中,并且具有相同的堆栈溢出。要解决此问题,您需要一种检测周期的方法。与https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof一样,但出于智力上的原因,我们将看看是否可以重新创建它们。

为此,我们需要记住 我们见过的所有其他物体 ,并在递归时与它们进行比较。一个简单的解决方案可能看起来像:

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]);
        }
    }
}

(注意:此循环功能具有破坏性。它修改了原始对象。此外,该循环功能不是递归的,因此实际上很糟糕。但是至少它给了您大致的概念,您可以尝试编写自己的,或者看看其他人是怎么做到的

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

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?而且我的简单循环算法无法解释对 root 对象的引用,它只记住所有 已经看到了(尽管考虑一下,添加起来应该很简单)

一种不是很完美但可以解决的解决方案

由于两个原因,我没有写出完美的深度相等实现:

  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;
}

更新

回应评论:

=====

==在两个变量之间执行“松散”比较。例如,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不是< / strong>(因为即使它们具有相同的值,相同的模式和相同的结构-它们引用的对象也不同)

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

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");

答案 1 :(得分:1)

我喜欢@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()方法对引用数组进行了浅表复制)。然后,每次看到对象或数组时,都要检查引用的历史记录,以查看它是否为循环引用。如果是这样,请确保两个循环引用都指向历史的同一部分(这很重要,因为两个循环引用都可能指向对象结构中的不同位置)。

由于我尚未对代码进行深入测试,所以我建议为此使用一个库,但是有一个简单的解决方案适合您。