比较两个对象是否相等

8

目前我正在学习一本书,但是对以下代码感到非常困惑,已经尝试多次理解。我的第一个疑问是如何比较两个对象ab

 function deepEqual(a, b) {
  if (a === b) return true;

  if (a == null || typeof a != "object" ||
      b == null || typeof b != "object")
    return false;

  var propsInA = 0, propsInB = 0;

  for (var prop in a)
    propsInA += 1;

  for (var prop in b) {
    propsInB += 1;
    if (!(prop in a) || !deepEqual(a[prop], b[prop]))
      return false;
  }

  return propsInA == propsInB;
}

var obj = {here: {is: "an"}, object: 2};
console.log(deepEqual(obj, obj));
// → true
console.log(deepEqual(obj, {here: 1, object: 2}));
// → false
console.log(deepEqual(obj, {here: {is: "an"}, object: 2}));
// → true

typeof在这里用来确保你在比较相同的事物。propsInX用来计算属性的数量,而不是比较值;如果计数不同,那么对象也是不同的。如果你只是通过查看第二个对象将{a:1,b:2}{a:1}进行比较,你会得到一个错误的结果。另一种方法是迭代两个对象并进行比较;计数器消除了对“双重”迭代的需求。 - dandavis
如果它们不是对象,也不相等,那么它的值为false(对象比较基于引用值而非对象值 - 这就是首先需要进行此类型检查的原因所在)。除此之外,如果它是一个对象并且有一个嵌套对象,则会递归遍历其属性。 - Travis J
!= 表示 "不等于"。 typeof a != "object" 表示 a 不是类型为 "object" 的对象。 - Juan Tomas
4个回答

3
function deepEqual(a, b) {
  if (a === b) return true;

首先,我们会检查 ab 是否严格相等(也就是说,它们是否完全相同)。大多数东西,比如字符串和数字,在相等时都会通过这个测试;但对象是一个例外,因为两个“相同”的对象并不一定是同一个对象(它们可能只是看起来一样)。
  if (a == null || typeof a != "object" ||
      b == null || typeof b != "object")
    return false;

那么我们说,如果两者中有任何一个不是对象,并且它们没有通过最后一个测试,那么它们就不能相同。再次说明,对象是例外情况,因此剩余的代码将处理ab都是对象的情况。

  var propsInA = 0, propsInB = 0;

  for (var prop in a)
    propsInA += 1;

这段代码简单地计算了 a 的属性数量。

  for (var prop in b) {
    propsInB += 1;
    if (!(prop in a) || !deepEqual(a[prop], b[prop]))
      return false;
  }

此代码遍历对象b的每个属性,并检查对象a是否具有相同的属性和值。如果对象a没有对象b的某个属性,或者它们的值不同,则这两个对象是不相等的。

  return propsInA == propsInB;
}

最后,如果ab的属性数量不相同,则它们不能相等。但是,如果它们具有相同数量的属性,则ab必须相等,因为a具有与b相同的所有属性,仅仅是这些。


1
我会带你走过这个过程。
if (a === b) return true;

我们检查这两个是否相同,稍后我们会回到这里。

if (a == null || typeof a != "object" ||
      b == null || typeof b != "object")
    return false;

我们检查这两个对象是否已定义,或者其中一个未定义。我们会在后面再回到这里。
记住这前两个片段,直到我们递归调用函数时才需要它们。
var propsInA = 0, propsInB = 0;

这些将被用来跟踪对象A和B中属性的数量。
for (var prop in a)
    propsInA += 1;

  for (var prop in b) {
    propsInB += 1;
    if (!(prop in a) || !deepEqual(a[prop], b[prop]))
      return false;
  }

  return propsInA == propsInB;
}

我们有两个for循环。第一个循环只是遍历A中的所有属性(如果您不熟悉,请查找For...in语法),并且对于每个属性它都增加变量propsInA。
第二个循环对B执行相同的操作,但这里变得更加复杂。首先,它检查该属性是否存在于A中,或者deepequal返回true。这就是我们考虑的前两个片段的作用。第一个片段在此处用于在我们给出相同属性时返回true。第二个片段表示“如果我们传递的是属性而不是函数,请停止”。这很重要,因为此函数只需要在初始调用后继续进行。所有递归调用只需要使用第一部分。如果这两个返回false,则向初始调用返回false,因为我们知道A和B之间存在差异。
return propsInA == propsInB;

我们不能在这里返回true,因为我们实际上并不知道B中是否只有较少的属性。即使其他所有内容都相同,我们也不能假设它们具有相同数量的属性。这可以作为最终检查来确保,仅当A中的属性数量等于B中的属性数量时才返回true。
欢迎进一步询问解释。

第一个情况,A有两个计数。第二种情况是它递归的原因(我一开始错过了这个)。在检查a.bob = b.bob之后,它尝试检查a.value,并且当它看到它是一个对象时,它再次通过前两个片段,而在这个新的调用中,它将a.value与b.value进行比较。在这个调用中,propsInA等于1,propsInB等于2。 - master565
我建议创建两个适当大小的对象,并尝试跟随代码的逻辑。确保每次函数调用都有自己的变量作用域。这是理解递归函数的最佳方法。 - master565
它正在比较某些东西。它只是在使用深度相等进行比较,在某些条件下将返回true。我们需要此行的原因是因为我们不仅需要查看对象是否具有所有相同的属性,还需要查看任何嵌套对象(如果a.property = {})。这必须进行递归处理。将deepequal视为2个函数可能会有所帮助。函数1是前两个代码段,函数2是其余部分。F1检查两个属性是否相等,而F2是对任何嵌套对象执行相同操作的递归调用。 - master565
不行。每次你在deepEqual内部调用它时,它都会创建一个新的作用域,在这个作用域中,每个变量都有一个新值。因此,在第1级别上,我们有2个属性。在第2级别上,我们有1个属性。虽然总数是3,但这并不重要。每个级别的属性数量才是重要的。 - master565
!(prop in a) 表示如果属性在a中不存在,我们将返回false(如果它不存在,我们不需要检查它是否相等)。它在循环中是因为我们需要检查b的所有属性,检查它们是否存在于a中,然后检查它们是否等于a。 - master565
显示剩余3条评论

0

在这个函数中

  • 首先,该函数简单地检查第一个参数是否等于第二个参数,然后返回true
  • typeof a! = "object" - 检查参数(a和b)的类型是否为对象,如果其中一个不是对象,则函数将通过返回false来结束。
  • 然后,如果它通过了这个条件(a和b都是对象),它将继续下一步-循环遍历对象(a和b)上的项目,并相应地计算propsInApropsInB
  • 下一步将是检查参数上是否没有项目,如果是,则返回false
  • 否则,函数将比较propsInApropsInB之间的差异,如果它们相同,则函数将通过返回true来结束,否则它将返回false

是的,这个函数可以处理多个嵌套项。关于你的第二个问题:propsInA和propsInB必须是“整数”,因为它们是计数器,该函数循环遍历每个参数的项目,并每次将+1添加到计数器中,然后将在计数器(propsInA和propsInB)之间进行比较。例如,如果a有5个项目,则propsInA将等于4(因为它从0开始)。 - user3378165

0

你的算法的行为如下:

  1. 如果 ab 被认为是严格相等的,则返回 true

    如果以下情况之一适用,则认为 ab 是严格相等的:

    • 它们是相同的原始值,但不包括 NaN
    • 它们是 +0-0(或反之亦然)
    • 它们是同一个对象(同一引用)。

    这是使用严格相等比较 (===) 运算符完成的。

  2. 如果 ab 或两者都不被视为对象,则返回 false

    如果以下情况之一适用,则不认为 o 是对象:

    • o 是原始值
    • o 属于 Object 类型,但具有内部 [[Call]] 方法。也就是说,它是可调用对象,例如函数或 HTML 的 <object> 元素。
    • o 是非可调用的非标准异类对象,其由 typeof 运算符返回的实现定义与 "object" 不同。

    这是使用 typeof 运算符完成的,它返回一个带有值类型的字符串,但不包括 null 和可能的对象。由于 typeof null === "object",因此检查 a == null

  3. 如果 ab 具有不同数量的可枚举属性(考虑自身和继承的属性),则返回 false

    这是通过计算 propsInApropsInB 来实现的。

  4. 如果 b 具有可枚举(自身或继承的)属性,但 a 没有具有相同名称的(非可枚举或可枚举,自身或继承的,不一定与 b 相同)属性,则返回 false

    这是通过迭代 for (var prop in b) 并检查 prop in a 来完成的。

  5. 如果 b 具有可枚举(自身或继承的)属性,其值被 deepEqual 算法视为与 a 中相同属性的值不同,则返回 false

    这是通过迭代 for (var prop in b) 并检查 deepEqual(a[prop], b[prop]) 来完成的。

  6. 否则,返回 true

我认为这不是一个好的算法。例如,它将{}Object.prototypeObject.create(null)视为相等,但我并不这么认为。


@cresjoy for (var prop in b) 迭代 b 的所有可枚举属性。对于每个属性名 prop,它会检查 a 是否有一个同名的属性 (prop in a),并且它们的值 a[prop]b[prop] 是否被认为相等。条件语句可以评估任何表达式,而不仅仅是将变量与文字进行比较。 - Oriol
不,当你发现一个属性不是ab共有的时候,你要返回false。但是如果你发现一个公共属性,你必须继续迭代其他属性,不能直接返回true。最后的return propsInA == propsInB将会在ab拥有相同数量的属性(并且它们是相同的,否则函数早就返回了false)时返回true。 - Oriol
如果变量prop中存储的字符串是对象a中某个属性(自有或继承)的名称,则prop in a返回true - Oriol
我认为我仍然对!(prop in a)感到困惑。这是在说:我们遍历b,如果a[prop]与b[prop]同时不存在,则返回false?也就是说,我们甚至不检查相等性。我们只是检查类型的数量是否匹配吗? - Muntasir Alam
@cresjoy 对象可以继承属性,例如'toString' in {}是从Object.prototype继承的。是的,prop in a只检查存在性。在deepEqual(a[prop], b[prop])中进行值的比较。 - Oriol
显示剩余5条评论

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