在JavaScript中比较对象的最佳方法是什么?

例子:

var user1 = {name : "nerd", org: "dev"};
var user2 = {name : "nerd", org: "dev"};
var eq = user1 == user2;
alert(eq); // gives false

我知道如果两个对象引用完全相同的对象,那么它们是相等的,但是有没有方法检查它们是否具有相同的属性值?

以下方式对我有效,但这是唯一的可能性吗?

var eq = Object.toJSON(user1) == Object.toJSON(user2);
alert(eq); // gives true

当前回答

以下是我的ES3注释解决方案(代码后的血腥细节):

function object_equals( x, y ) {
  if ( x === y ) return true;
    // if both x and y are null or undefined and exactly the same

  if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
    // if they are not strictly equal, they both need to be Objects

  if ( x.constructor !== y.constructor ) return false;
    // they must have the exact same prototype chain, the closest we can do is
    // test there constructor.

  for ( var p in x ) {
    if ( ! x.hasOwnProperty( p ) ) continue;
      // other properties were tested using x.constructor === y.constructor

    if ( ! y.hasOwnProperty( p ) ) return false;
      // allows to compare x[ p ] and y[ p ] when set to undefined

    if ( x[ p ] === y[ p ] ) continue;
      // if they have the same strict value or identity then they are equal

    if ( typeof( x[ p ] ) !== "object" ) return false;
      // Numbers, Strings, Functions, Booleans must be strictly equal

    if ( ! object_equals( x[ p ],  y[ p ] ) ) return false;
      // Objects and Arrays must be tested recursively
  }

  for ( p in y )
    if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) )
      return false;
        // allows x[ p ] to be set to undefined

  return true;
}

在开发这个解决方案的过程中,我特别关注了角落的情况,效率,但试图产生一个简单的解决方案,希望能有一些优雅。JavaScript允许空的和未定义的财产,对象具有原型链,如果不进行检查,可能会导致非常不同的行为。

首先,我选择不扩展Object.prototype,主要是因为null不能作为比较对象之一,并且我认为null应该是一个有效的对象,可以与其他对象进行比较。其他人也注意到了Object.prototype的扩展对其他代码可能产生的副作用。

必须特别注意处理JavaScript允许对象财产设置为未定义的可能性,即存在值设置为未确定的财产。上述解决方案验证了这两个对象是否具有相同的设置为未定义的财产以报告相等性。这只能通过使用Object.hasOwnProperty(property_name)检查财产是否存在来完成。还要注意,JSON.stringify()删除了设置为未定义的财产,因此使用此表单进行比较时将忽略设置为未确定值的财产。

只有当函数共享相同的引用,而不仅仅是相同的代码时,才应该认为它们是相等的,因为这不会考虑这些函数原型。因此,比较代码字符串不能保证它们具有相同的原型对象。

这两个对象应该具有相同的原型链,而不仅仅是相同的财产。这只能通过比较两个对象的构造函数以确保严格相等来跨浏览器测试。ECMAScript 5将允许使用Object.getPrototypeOf()测试他们的实际原型。一些web浏览器还提供了__proto__属性,该属性可以执行相同的操作。对上述代码的一个可能的改进将允许在任何可用的情况下使用这些方法之一。

这里使用严格比较是最重要的,因为2不应被视为等于“2.0000”,false也不应被认为等于null、undefined或0。

出于效率考虑,我会尽快比较财产的相等性。然后,只有在这失败的情况下,才能查找这些财产的类型。速度提升对于具有大量标量财产的大型对象来说意义重大。

不再需要两个循环,第一个循环从左侧对象检查财产,第二个循环从右侧检查财产并仅验证存在性(而不是值),以捕获使用未定义值定义的这些财产。

总的来说,这段代码只需要16行代码(没有注释)就可以处理大部分的角情况。


更新(2015年8月13日)。我实现了一个更好的版本,因为函数value_equals()速度更快,能够正确地处理诸如NaN和0不同于-0的拐角情况,可以选择强制对象的财产顺序并测试循环引用,作为Toubkal项目测试套件的一部分,它由100多个自动测试支持。

其他回答

  Utils.compareObjects = function(o1, o2){
    for(var p in o1){
        if(o1.hasOwnProperty(p)){
            if(o1[p] !== o2[p]){
                return false;
            }
        }
    }
    for(var p in o2){
        if(o2.hasOwnProperty(p)){
            if(o1[p] !== o2[p]){
                return false;
            }
        }
    }
    return true;
};

比较ONE-LEVEL对象的简单方法。

我修改了上面的代码。对于我0!==false和null!==未定义。如果不需要这样严格的检查,请在代码中删除一个“=”sign-in“this[p]!==x[p]”。

Object.prototype.equals = function(x){
    for (var p in this) {
        if(typeof(this[p]) !== typeof(x[p])) return false;
        if((this[p]===null) !== (x[p]===null)) return false;
        switch (typeof(this[p])) {
            case 'undefined':
                if (typeof(x[p]) != 'undefined') return false;
                break;
            case 'object':
                if(this[p]!==null && x[p]!==null && (this[p].constructor.toString() !== x[p].constructor.toString() || !this[p].equals(x[p]))) return false;
                break;
            case 'function':
                if (p != 'equals' && this[p].toString() != x[p].toString()) return false;
                break;
            default:
                if (this[p] !== x[p]) return false;
        }
    }
    return true;
}

然后我用下一个对象测试了它:

var a = {a: 'text', b:[0,1]};
var b = {a: 'text', b:[0,1]};
var c = {a: 'text', b: 0};
var d = {a: 'text', b: false};
var e = {a: 'text', b:[1,0]};
var f = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var g = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var h = {a: 'text', b:[1,0], f: function(){ this.a = this.b; }};
var i = {
    a: 'text',
    c: {
        b: [1, 0],
        f: function(){
            this.a = this.b;
        }
    }
};
var j = {
    a: 'text',
    c: {
        b: [1, 0],
        f: function(){
            this.a = this.b;
        }
    }
};
var k = {a: 'text', b: null};
var l = {a: 'text', b: undefined};

a==b预期为真;返回true

a==c预期为假;返回false

c==d预期为假;返回false

a==e预期为假;返回false

f==g预期为真;返回true

h==g预期为假;返回false

i==j预期为真;返回true

d==k预期为假;返回false

k==l预期为假;返回false

当然不是唯一的方法——您可以原型化一个方法(在这里针对Object,但我当然不建议将Object用于实时代码)来复制C#/Java风格的比较方法。

编辑,因为似乎需要一个通用示例:

Object.prototype.equals = function(x)
{
    for(p in this)
    {
        switch(typeof(this[p]))
        {
            case 'object':
                if (!this[p].equals(x[p])) { return false }; break;
            case 'function':
                if (typeof(x[p])=='undefined' || (p != 'equals' && this[p].toString() != x[p].toString())) { return false; }; break;
            default:
                if (this[p] != x[p]) { return false; }
        }
    }

    for(p in x)
    {
        if(typeof(this[p])=='undefined') {return false;}
    }

    return true;
}

请注意,使用toString()测试方法绝对不够好,但一个可以接受的方法非常困难,因为空格是否有意义,更不用说同义词方法和使用不同实现产生相同结果的方法。以及针对Object的原型设计问题。

以下算法将处理自引用数据结构、数字、字符串、日期,当然还有普通的嵌套javascript对象:

根据==,它们完全相等(字符串和数字首先展开,以确保42等于数字(42))或者它们都是日期并且具有相同的值Of()或者它们都是同一类型且不是空的。。。它们不是对象,每个==都相等(捕获数字/字符串/布尔值)或者,忽略具有未定义值的财产,它们具有相同的财产,所有这些属性都被视为递归等价。

函数文本不认为函数相同。此测试不够,因为函数可能具有不同的闭包。只有当==这样说时,函数才被认为是相等的(但如果您选择这样做,您可以很容易地扩展等效关系)。

避免了循环数据结构可能导致的无限循环。当areEquivalent试图反驳等式并递归到对象的财产中时,它会跟踪需要进行此子比较的对象。如果等式可以被否定,那么对象之间的某些可达属性路径不同,那么必须有一条最短的可达路径,并且该最短可达路径不能包含两条路径中存在的循环;即在递归比较对象时假设相等是可以的。假设存储在属性areEquivalent_Eq_91_2_34中,该属性在使用后被删除,但如果对象图已经包含此类属性,则行为未定义。使用这样的标记属性是必要的,因为javascript不支持使用任意对象作为键的字典。

function unwrapStringOrNumber(obj) {
    return (obj instanceof Number || obj instanceof String 
            ? obj.valueOf() 
            : obj);
}
function areEquivalent(a, b) {
    a = unwrapStringOrNumber(a);
    b = unwrapStringOrNumber(b);
    if (a === b) return true; //e.g. a and b both null
    if (a === null || b === null || typeof (a) !== typeof (b)) return false;
    if (a instanceof Date) 
        return b instanceof Date && a.valueOf() === b.valueOf();
    if (typeof (a) !== "object") 
        return a == b; //for boolean, number, string, xml

    var newA = (a.areEquivalent_Eq_91_2_34 === undefined),
        newB = (b.areEquivalent_Eq_91_2_34 === undefined);
    try {
        if (newA) a.areEquivalent_Eq_91_2_34 = [];
        else if (a.areEquivalent_Eq_91_2_34.some(
            function (other) { return other === b; })) return true;
        if (newB) b.areEquivalent_Eq_91_2_34 = [];
        else if (b.areEquivalent_Eq_91_2_34.some(
            function (other) { return other === a; })) return true;
        a.areEquivalent_Eq_91_2_34.push(b);
        b.areEquivalent_Eq_91_2_34.push(a);

        var tmp = {};
        for (var prop in a) 
            if(prop != "areEquivalent_Eq_91_2_34") 
                tmp[prop] = null;
        for (var prop in b) 
            if (prop != "areEquivalent_Eq_91_2_34") 
                tmp[prop] = null;

        for (var prop in tmp) 
            if (!areEquivalent(a[prop], b[prop]))
                return false;
        return true;
    } finally {
        if (newA) delete a.areEquivalent_Eq_91_2_34;
        if (newB) delete b.areEquivalent_Eq_91_2_34;
    }
}

以下是我的ES3注释解决方案(代码后的血腥细节):

function object_equals( x, y ) {
  if ( x === y ) return true;
    // if both x and y are null or undefined and exactly the same

  if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
    // if they are not strictly equal, they both need to be Objects

  if ( x.constructor !== y.constructor ) return false;
    // they must have the exact same prototype chain, the closest we can do is
    // test there constructor.

  for ( var p in x ) {
    if ( ! x.hasOwnProperty( p ) ) continue;
      // other properties were tested using x.constructor === y.constructor

    if ( ! y.hasOwnProperty( p ) ) return false;
      // allows to compare x[ p ] and y[ p ] when set to undefined

    if ( x[ p ] === y[ p ] ) continue;
      // if they have the same strict value or identity then they are equal

    if ( typeof( x[ p ] ) !== "object" ) return false;
      // Numbers, Strings, Functions, Booleans must be strictly equal

    if ( ! object_equals( x[ p ],  y[ p ] ) ) return false;
      // Objects and Arrays must be tested recursively
  }

  for ( p in y )
    if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) )
      return false;
        // allows x[ p ] to be set to undefined

  return true;
}

在开发这个解决方案的过程中,我特别关注了角落的情况,效率,但试图产生一个简单的解决方案,希望能有一些优雅。JavaScript允许空的和未定义的财产,对象具有原型链,如果不进行检查,可能会导致非常不同的行为。

首先,我选择不扩展Object.prototype,主要是因为null不能作为比较对象之一,并且我认为null应该是一个有效的对象,可以与其他对象进行比较。其他人也注意到了Object.prototype的扩展对其他代码可能产生的副作用。

必须特别注意处理JavaScript允许对象财产设置为未定义的可能性,即存在值设置为未确定的财产。上述解决方案验证了这两个对象是否具有相同的设置为未定义的财产以报告相等性。这只能通过使用Object.hasOwnProperty(property_name)检查财产是否存在来完成。还要注意,JSON.stringify()删除了设置为未定义的财产,因此使用此表单进行比较时将忽略设置为未确定值的财产。

只有当函数共享相同的引用,而不仅仅是相同的代码时,才应该认为它们是相等的,因为这不会考虑这些函数原型。因此,比较代码字符串不能保证它们具有相同的原型对象。

这两个对象应该具有相同的原型链,而不仅仅是相同的财产。这只能通过比较两个对象的构造函数以确保严格相等来跨浏览器测试。ECMAScript 5将允许使用Object.getPrototypeOf()测试他们的实际原型。一些web浏览器还提供了__proto__属性,该属性可以执行相同的操作。对上述代码的一个可能的改进将允许在任何可用的情况下使用这些方法之一。

这里使用严格比较是最重要的,因为2不应被视为等于“2.0000”,false也不应被认为等于null、undefined或0。

出于效率考虑,我会尽快比较财产的相等性。然后,只有在这失败的情况下,才能查找这些财产的类型。速度提升对于具有大量标量财产的大型对象来说意义重大。

不再需要两个循环,第一个循环从左侧对象检查财产,第二个循环从右侧检查财产并仅验证存在性(而不是值),以捕获使用未定义值定义的这些财产。

总的来说,这段代码只需要16行代码(没有注释)就可以处理大部分的角情况。


更新(2015年8月13日)。我实现了一个更好的版本,因为函数value_equals()速度更快,能够正确地处理诸如NaN和0不同于-0的拐角情况,可以选择强制对象的财产顺序并测试循环引用,作为Toubkal项目测试套件的一部分,它由100多个自动测试支持。