Object comparison in JavaScript [duplicate]

2018-12-30 22:56发布

This question already has an answer here:

What is the best way to compare objects in JavaScript?

Example:

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

I know that two objects are equal if they refer to the exact same object, but is there a way to check if they have the same attributes' values?

The following way works for me, but is it the only possibility?

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

10条回答
后来的你喜欢了谁
2楼-- · 2018-12-30 23:10

I have modified a bit the code above. for me 0 !== false and null !== undefined. If you do not need such strict check remove one "=" sign in "this[p] !== x[p]" inside the code.

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

Then I have tested it with next objects:

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 expected true; returned true

a==c expected false; returned false

c==d expected false; returned false

a==e expected false; returned false

f==g expected true; returned true

h==g expected false; returned false

i==j expected true; returned true

d==k expected false; returned false

k==l expected false; returned false

查看更多
孤独总比滥情好
3楼-- · 2018-12-30 23:11

Here is my commented solution in ES3 (gory details after the code):

Object.equals = function( 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;
}

In developing this solution, I took a particular look at corner cases, efficiency, yet trying to yield a simple solution that works, hopefully with some elegance. JavaScript allows both null and undefined properties and objects have prototypes chains that can lead to very different behaviors if not checked.

First I have chosen to extend Object instead of Object.prototype, mostly because null could not be one of the objects of the comparison and that I believe that null should be a valid object to compare with another. There are also other legitimate concerns noted by others regarding the extension of Object.prototype regarding possible side effects on other's code.

Special care must taken to deal the possibility that JavaScript allows object properties can be set to undefined, i.e. there exists properties which values are set to undefined. The above solution verifies that both objects have the same properties set to undefined to report equality. This can only be accomplished by checking the existence of properties using Object.hasOwnProperty( property_name ). Also note that JSON.stringify() removes properties that are set to undefined, and that therefore comparisons using this form will ignore properties set to the value undefined.

Functions should be considered equal only if they share the same reference, not just the same code, because this would not take into account these functions prototype. So comparing the code string does not work to guaranty that they have the same prototype object.

The two objects should have the same prototype chain, not just the same properties. This can only be tested cross-browser by comparing the constructor of both objects for strict equality. ECMAScript 5 would allow to test their actual prototype using Object.getPrototypeOf(). Some web browsers also offer a __proto__ property that does the same thing. A possible improvement of the above code would allow to use one of these methods whenever available.

The use of strict comparisons is paramount here because 2 should not be considered equal to "2.0000", nor false should be considered equal to null, undefined, or 0.

Efficiency considerations lead me to compare for equality of properties as soon as possible. Then, only if that failed, look for the typeof these properties. The speed boost could be significant on large objects with lots of scalar properties.

No more that two loops are required, the first to check properties from the left object, the second to check properties from the right and verify only existence (not value), to catch these properties which are defined with the undefined value.

Overall this code handles most corner cases in only 16 lines of code (without comments).

Update (8/13/2015). I have implemented a better version, as the function value_equals() that is faster, handles properly corner cases such as NaN and 0 different than -0, optionally enforcing objects' properties order and testing for cyclic references, backed by more than 100 automated tests as part of the Toubkal project test suite.

查看更多
零度萤火
4楼-- · 2018-12-30 23:13

Unfortunately there is no perfect way, unless you use _proto_ recursively and access all non-enumerable properties, but this works in Firefox only.

So the best I can do is to guess usage scenarios.


1) Fast and limited.

Works when you have simple JSON-style objects without methods and DOM nodes inside:

 JSON.stringify(obj1) === JSON.stringify(obj2) 

The ORDER of the properties IS IMPORTANT, so this method will return false for following objects:

 x = {a: 1, b: 2};
 y = {b: 2, a: 1};

2) Slow and more generic.

Compares objects without digging into prototypes, then compares properties' projections recursively, and also compares constructors.

This is almost correct algorithm:

function deepCompare () {
  var i, l, leftChain, rightChain;

  function compare2Objects (x, y) {
    var p;

    // remember that NaN === NaN returns false
    // and isNaN(undefined) returns true
    if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
         return true;
    }

    // Compare primitives and functions.     
    // Check if both arguments link to the same object.
    // Especially useful on the step where we compare prototypes
    if (x === y) {
        return true;
    }

    // Works in case when functions are created in constructor.
    // Comparing dates is a common scenario. Another built-ins?
    // We can even handle functions passed across iframes
    if ((typeof x === 'function' && typeof y === 'function') ||
       (x instanceof Date && y instanceof Date) ||
       (x instanceof RegExp && y instanceof RegExp) ||
       (x instanceof String && y instanceof String) ||
       (x instanceof Number && y instanceof Number)) {
        return x.toString() === y.toString();
    }

    // At last checking prototypes as good as we can
    if (!(x instanceof Object && y instanceof Object)) {
        return false;
    }

    if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
        return false;
    }

    if (x.constructor !== y.constructor) {
        return false;
    }

    if (x.prototype !== y.prototype) {
        return false;
    }

    // Check for infinitive linking loops
    if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
         return false;
    }

    // Quick checking of one object being a subset of another.
    // todo: cache the structure of arguments[0] for performance
    for (p in y) {
        if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
            return false;
        }
        else if (typeof y[p] !== typeof x[p]) {
            return false;
        }
    }

    for (p in x) {
        if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
            return false;
        }
        else if (typeof y[p] !== typeof x[p]) {
            return false;
        }

        switch (typeof (x[p])) {
            case 'object':
            case 'function':

                leftChain.push(x);
                rightChain.push(y);

                if (!compare2Objects (x[p], y[p])) {
                    return false;
                }

                leftChain.pop();
                rightChain.pop();
                break;

            default:
                if (x[p] !== y[p]) {
                    return false;
                }
                break;
        }
    }

    return true;
  }

  if (arguments.length < 1) {
    return true; //Die silently? Don't know how to handle such case, please help...
    // throw "Need two or more arguments to compare";
  }

  for (i = 1, l = arguments.length; i < l; i++) {

      leftChain = []; //Todo: this can be cached
      rightChain = [];

      if (!compare2Objects(arguments[0], arguments[i])) {
          return false;
      }
  }

  return true;
}

Known issues (well, they have very low priority, probably you'll never notice them):

  • objects with different prototype structure but same projection
  • functions may have identical text but refer to different closures

Tests: passes tests are from How to determine equality for two JavaScript objects?.

查看更多
步步皆殇っ
5楼-- · 2018-12-30 23:13

The following algorithm will deal with self-referential data structures, numbers, strings, dates, and of course plain nested javascript objects:

Objects are considered equivalent when

  • They are exactly equal per === (String and Number are unwrapped first to ensure 42 is equivalent to Number(42))
  • or they are both dates and have the same valueOf()
  • or they are both of the same type and not null and...
    • they are not objects and are equal per == (catches numbers/strings/booleans)
    • or, ignoring properties with undefined value they have the same properties all of which are considered recursively equivalent.

Functions are not considered identical by function text. This test is insufficient because functions may have differing closures. Functions are only considered equal if === says so (but you could easily extend that equivalent relation should you choose to do so).

Infinite loops, potentially caused by circular datastructures, are avoided. When areEquivalent attempts to disprove equality and recurses into an object's properties to do so, it keeps track of the objects for which this sub-comparison is needed. If equality can be disproved, then some reachable property path differs between the objects, and then there must be a shortest such reachable path, and that shortest reachable path cannot contain cycles present in both paths; i.e. it is OK to assume equality when recursively comparing objects. The assumption is stored in a property areEquivalent_Eq_91_2_34, which is deleted after use, but if the object graph already contains such a property, behavior is undefined. The use of such a marker property is necessary because javascript doesn't support dictionaries using arbitrary objects as keys.

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;
    }
}
查看更多
零度萤火
6楼-- · 2018-12-30 23:21

Certainly not the only way - you could prototype a method (against Object here but I certainly wouldn't suggest using Object for live code) to replicate C#/Java style comparison methods.

Edit, since a general example seems to be expected:

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

Note that testing methods with toString() is absolutely not good enough but a method which would be acceptable is very hard because of the problem of whitespace having meaning or not, never mind synonym methods and methods producing the same result with different implementations. And the problems of prototyping against Object in general.

查看更多
姐姐魅力值爆表
7楼-- · 2018-12-30 23:23

I wrote this piece of code for object comparison, and it seems to work. check the assertions:


function countProps(obj) {
    var count = 0;
    for (k in obj) {
        if (obj.hasOwnProperty(k)) {
            count++;
        }
    }
    return count;
};

function objectEquals(v1, v2) {

    if (typeof(v1) !== typeof(v2)) {
        return false;
    }

    if (typeof(v1) === "function") {
        return v1.toString() === v2.toString();
    }

    if (v1 instanceof Object && v2 instanceof Object) {
        if (countProps(v1) !== countProps(v2)) {
            return false;
        }
        var r = true;
        for (k in v1) {
            r = objectEquals(v1[k], v2[k]);
            if (!r) {
                return false;
            }
        }
        return true;
    } else {
        return v1 === v2;
    }
}

assert.isTrue(objectEquals(null,null));
assert.isFalse(objectEquals(null,undefined));

assert.isTrue(objectEquals("hi","hi"));
assert.isTrue(objectEquals(5,5));
assert.isFalse(objectEquals(5,10));

assert.isTrue(objectEquals([],[]));
assert.isTrue(objectEquals([1,2],[1,2]));
assert.isFalse(objectEquals([1,2],[2,1]));
assert.isFalse(objectEquals([1,2],[1,2,3]));

assert.isTrue(objectEquals({},{}));
assert.isTrue(objectEquals({a:1,b:2},{a:1,b:2}));
assert.isTrue(objectEquals({a:1,b:2},{b:2,a:1}));
assert.isFalse(objectEquals({a:1,b:2},{a:1,b:3}));

assert.isTrue(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}));
assert.isFalse(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:27}}));

assert.isTrue(objectEquals(function(x){return x;},function(x){return x;}));
assert.isFalse(objectEquals(function(x){return x;},function(y){return y+2;}));
查看更多
登录 后发表回答