对象/数组的深度比较

38
如果我有两个数组或对象想要进行比较,例如:

可能是重复问题:
如何确定两个JavaScript对象是否相等?
JavaScript中的对象比较

object1 = [
 { shoes:
   [ 'loafer', 'penny' ]
  },
  { beers:
     [ 'budweiser', 'busch' ]
  }
]

object2 = [
 { shoes:
   [ 'loafer', 'penny' ]
  },
  { beers:
     [ 'budweiser', 'busch' ]
  }
]

object1 == object2 // false

如果你正在等待服务器的响应并尝试查看是否已更改,那么这可能会很烦人。


相关:https://dev59.com/xXVC5IYBdhLWcg3wtzut。 - Juho Vepsäläinen
2个回答

36

更新:
针对原始建议(比较2个JSON字符串)所引起的评论和担忧,您可以使用此函数:

function compareObjects(o, p)
{
    var i,
        keysO = Object.keys(o).sort(),
        keysP = Object.keys(p).sort();
    if (keysO.length !== keysP.length)
        return false;//not the same nr of keys
    if (keysO.join('') !== keysP.join(''))
        return false;//different keys
    for (i=0;i<keysO.length;++i)
    {
        if (o[keysO[i]] instanceof Array)
        {
            if (!(p[keysO[i]] instanceof Array))
                return false;
            //if (compareObjects(o[keysO[i]], p[keysO[i]] === false) return false
            //would work, too, and perhaps is a better fit, still, this is easy, too
            if (p[keysO[i]].sort().join('') !== o[keysO[i]].sort().join(''))
                return false;
        }
        else if (o[keysO[i]] instanceof Date)
        {
            if (!(p[keysO[i]] instanceof Date))
                return false;
            if ((''+o[keysO[i]]) !== (''+p[keysO[i]]))
                return false;
        }
        else if (o[keysO[i]] instanceof Function)
        {
            if (!(p[keysO[i]] instanceof Function))
                return false;
            //ignore functions, or check them regardless?
        }
        else if (o[keysO[i]] instanceof Object)
        {
            if (!(p[keysO[i]] instanceof Object))
                return false;
            if (o[keysO[i]] === o)
            {//self reference?
                if (p[keysO[i]] !== p)
                    return false;
            }
            else if (compareObjects(o[keysO[i]], p[keysO[i]]) === false)
                return false;//WARNING: does not deal with circular refs other than ^^
        }
        if (o[keysO[i]] !== p[keysO[i]])//change !== to != for loose comparison
            return false;//not the same value
    }
    return true;
}

但在许多情况下,我认为它不必那么困难:

JSON.stringify(object1) === JSON.stringify(object2);

如果对象的字符串化版本相同,则它们的值是相似的。
为了完整起见:JSON仅仅忽略函数(也就是全部删除)。它旨在表示数据,而不是功能性
试图比较只包含函数的2个对象将导致true

JSON.stringify({foo: function(){return 1;}}) === JSON.stringify({foo: function(){ return -1;}});
//evaulutes to:
'{}' === '{}'
//is true, of course

如果想要深度比较对象/函数,你需要使用库或编写自己的函数,并克服JS对象都是引用的事实,因此在比较 o1 === ob2 时,只有在两个变量指向同一个对象时才会返回true...

正如@a-j在评论中所指出的:

JSON.stringify({a: 1, b: 2}) === JSON.stringify({b: 2, a: 1});

由于两次字符串化调用都产生了 "{"a":1,"b":2}""{"b":2,"a":1}",所以false. 为什么会这样?你需要了解 Chrome 的 V8 引擎内部机制。我不是专家,简单说一下就是:

V8 引擎在创建每个对象和修改它们时都会创建一个新的隐藏 C++ 类。如果对象 X 有一个属性 a,而另一个对象也有相同的属性,那么这两个 JS 对象将引用一个继承自定义有该属性 a 的共享隐藏类。如果两个对象共享相同的基本属性,则它们将引用相同的隐藏类,JSON.stringify 将完全相同地对这两个对象进行处理。这是确定的(如果您感兴趣,可以在这里了解更多有关 V8 内部的详细信息)。

但是,在 a-j 指出的示例中,这两个对象被串行化为不同的字符串。为什么呢?简单地说,因为这些对象从未同时存在过:

JSON.stringify({a: 1, b: 2})
这是一个函数调用,需要解析为结果值后才能与右操作数进行比较。第二个对象字面量还没有被处理。
该对象被字符串化,表达式被解析为一个字符串常量。对象字面量没有被引用到任何地方,并被标记为待回收的垃圾。
在此之后,右操作数(JSON.stringify({b: 2, a: 1})表达式)也会接受同样的处理。
这些都很好,但需要考虑的是现在的JavaScript引擎比以前要复杂得多。我不是V8专家,但我认为a-j的代码片段正在进行大量优化,即代码被优化为:
"{"b":2,"a":1}" === "{"a":1,"b":2}"

基本上完全省略JSON.stringify的调用,只需在正确的位置添加引号。毕竟,这样更加有效率。


25
我认为JSON.stringify不保证输出的顺序。 JSON.stringify({a: 1, b: 1}) 可能会变成 '{"a": 1, "b": 1}' 或者 '{"b": 1, "a": 1}',然后比较就会失败。 - Tony Arkles
@TonyArkles:属性的表示顺序没有任何标准(ECMA将其留给实现)。但是,可以说具有相同属性的2个对象将以相同的方式进行字符串化:两个对象由相同的实现进行字符串化,如果两个对象都定义了相同的属性,则它们的输出将是相同的。JSON不排序任何内容,但例如V8引擎会按字母顺序对属性进行排序。因此{b:1,a:1}是不可能的... - Elias Van Ootegem
@j-a:我不想显得太学究,但我必须反对:如果引擎被调整或更新,并且JSON.stringify的业务发生变化,则这些更改将适用于您要比较的两个对象的字符串化。我认为无论哪种方式输出都不会有任何不同。但是,我会添加一种替代方案,这种方案不会很快发生变化。 - Elias Van Ootegem
1
@pomo:不要认为那是故意的(这是一个4.5年前的回答)。可能忘了对数组元素进行递归调用,或者可能与原始问题或某个评论有关。 - Elias Van Ootegem
1
@Michal:对于那些花时间批评一个5年前的代码片段的人来说,你竟然不能理解return truereturn false的含义(是的,函数返回布尔值,如果相等则为true,否则为false)。op显然是参数,考虑到该函数被称为compareObjects,它们可能是_对象_(因此是o)。无论如何:是的,名称不好,风格也不好……但再说一遍:这是5年前的代码……如果我仍在做JS,我会用ES6/ES2016完全不同的方式编写它,或者将其命名为objectEquals或其他名称。 - Elias Van Ootegem
显示剩余13条评论

3
作为下划线的混合组件:
在咖啡脚本中:
_.mixin deepEquals: (ar1, ar2) ->

    # typeofs should match
    return false unless (_.isArray(ar1) and _.isArray(ar2)) or (_.isObject(ar1) and _.isObject(ar2))

    #lengths should match
    return false if ar1.length != ar2.length

    still_matches = true

    _fail = -> still_matches = false

    _.each ar1, (prop1, n) =>

      prop2 = ar2[n]

      return if prop1 == prop2

      _fail() unless _.deepEquals prop1, prop2

    return still_matches

而在 JavaScript 中:

_.mixin({
  deepEquals: function(ar1, ar2) {
    var still_matches, _fail,
      _this = this;
    if (!((_.isArray(ar1) && _.isArray(ar2)) || (_.isObject(ar1) && _.isObject(ar2)))) {
      return false;
    }
    if (ar1.length !== ar2.length) {
      return false;
    }
    still_matches = true;
    _fail = function() {
      still_matches = false;
    };
    _.each(ar1, function(prop1, n) {
      var prop2;
      prop2 = ar2[n];
      if (prop1 !== prop2 && !_.deepEquals(prop1, prop2)) {
        _fail();
      }
    });
    return still_matches;
  }
});

Funkodebat,你传递给underscore each的迭代器函数具有不一致的返回点: 如果(prop1 === prop2){ 返回; /* void */ } 如果(!_.deepEquals(prop1,prop2)){ 返回_fail(); /* false */ } - Sorin Postelnicu
返回迭代器的目的是什么?据我所知,_.each不会中断循环(如果这是你想要的)。 - Sorin Postelnicu
还是这只是来自coffeescript的半自动转换,其中每个语句都有一个返回值,因此当调用“_fail() unless _.deepEquals prop1,prop2”时,它将其转换为“return _fail()”? - Sorin Postelnicu
是的,CoffeeScript 只会返回每个函数的最后一行。 - dansch
下划线的 _.isEqual() 和 isEqual() 有区别吗?请参见 https://underscorejs.org/#isEqual ,它说:“执行两个对象之间的优化深度比较,以确定它们是否应被视为相等。” - Peter T.

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