Deep comparison of objects/arrays [duplicate]
Asked Answered
H

2

40

Possible Duplicate:
How do you determine equality for two JavaScript objects?
Object comparison in JavaScript

If I have two arrays or objects and want to compare them, such as

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

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

object1 == object2 // false

this can be annoying if you're getting a response from a server and trying to see if it's changed

Horick answered 30/10, 2012 at 15:59 Comment(1)
Related: #201683 .Shanahan
M
37

Update:
In response to the comments and worries surrounding the original suggestion (comparing 2 JSON strings), you could use this function:

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

But in many cases, it needn't be that difficult IMO:

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

If the stringified objects are the same, their values are alike.
For completeness' sake: JSON simply ignores functions (well, removes them all together). It's meant to represent Data, not functionality.
Attempting to compare 2 objects that contain only functions will result in true:

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

For deep-comparison of objects/functions, you'll have to turn to libs or write your own function, and overcome the fact that JS objects are all references, so when comparing o1 === ob2 it'll only return true if both variables point to the same object...

As @a-j pointed out in the comment:

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

is false, as both stringify calls yield "{"a":1,"b":2}" and "{"b":2,"a":1}" respectively. As to why this is, you need to understand the internals of chrome's V8 engine. I'm not an expert, and without going into too much detail, here's what it boils down to:

Each object that is created, and each time it is modified, V8 creates a new hidden C++ class (sort of). If object X has a property a, and another object has the same property, both these JS objects will reference a hidden class that inherits from a shared hidden class that defines this property a. If two objects all share the same basic properties, then they will all reference the same hidden classes, and JSON.stringify will work exactly the same on both objects. That's a given (More details on V8's internals here, if you're interested).

However, in the example pointed out by a-j, both objects are stringified differently. How come? Well, put simply, these objects never exist at the same time:

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

This is a function call, an expression that needs to be resolved to the resulting value before it can be compared to the right-hand operand. The second object literal isn't on the table yet.
The object is stringified, and the exoression is resolved to a string constant. The object literal isn't being referenced anywhere and is flagged for garbage collection.
After this, the right hand operand (the JSON.stringify({b: 2, a: 1}) expression) gets the same treatment.

All fine and dandy, but what also needs to be taken into consideration is that JS engines now are far more sophisticated than they used to be. Again, I'm no V8 expert, but I think its plausible that a-j's snippet is being heavily optimized, in that the code is optimized to:

"{"b":2,"a":1}" === "{"a":1,"b":2}"

Essentially omitting the JSON.stringify calls all together, and just adding quotes in the right places. That is, after all, a lot more efficient.

Maize answered 30/10, 2012 at 16:4 Comment(18)
This isn't necessarily true. This doesn't encompass all cases, ie functions as a value, objects as values -- you may end up with lots of [Object Object] - which may be a false positive.Pulpy
@ansiart: I never claimed this is a universal sollution. The OP wanted to compare two objects, like the ones in his question. To acchieve that, this answer is the easiest way.Maize
I don't believe JSON.stringify has any guarantee of the ordering of the output. JSON.stringify({a: 1, b: 1}) could turn into '{"a": 1, "b": 1}' or '{"b": 1, "a": 1}' and the comparison will fail.Citify
@TonyArkles: The order in which properties are represented aren't gouverned by any standard (ECMA leaves that up to the implementation). However, it's fair to say that 2 object, with the same properties, will be stringified in the same way: both objects are stringified by the same implementation, and their output will be identical, if both objects have the same properties defined. JSON doesn't order anything, but the V8 engine, for example orders the properties alphabetically. so {b:1,a:1} is an impossibility...Maize
@EliasVanOotegem That's not true, at least for Chrome. The order matches the order in which properties are added. For example: var o = {b: 1}; o.a = 1; results in {b: 1, a: 1}, and also stringified that way. It is true, though, that order is predictable on V8 either way. Also seems to be true of latest FireFox.Johnny
@EliasVanOotegem in chrome (Version 33.0.1750.152): JSON.stringify({'a':1,'b':2}) === JSON.stringify({'b':2, 'a':1}); gives false. So while convenient, it is really only for a particular use case.Plaid
@j-a: You're right, but your example is the exception here, I'm afraid to say. write var a = {b: 2, a: 1}, b = {a:1, b: 2}; console.log(JSON.stringify(a) === JSON.stringify(b)); and you get true: Owing to how V8 translates objects to hidden classes, and there being a sequence pointer, the objects' properties will be ordered alphabetically. The case of JSON.stringify({objectliteral: 'x}) === JSON.stringify({objectliteral: 'y'}); is purely theoretical, but I'll add that to the answerMaize
Elias, thanks for the update. Interesting. I think though that it may come back to the point @TonyArkles raised, that stringify does not promise an order. I.e. that may hit code depending on it in an update to e.g. V8 (as they are free to optimize, or change etc as long as they are compliant.) Would you agree?Plaid
@j-a: I don't want to come over all pedantic, but I'd have to disagree: if an engine is tweaked or updated, and the JSON.stringify business changes, those changes apply to both objects you are stringifying to compare. I see no reason why the output would be any different either way. However, I'll add an alternative that won't be subject to change any time soonMaize
Both a/b examples give false for me in Chrome 43/Win (and indeed real-world code). Don't rely on JSON.stringify order.Chrisy
funciton typo on line one indicates no one ran this code. :)Casiano
@teknopaul: Fixed typo. I don't think I copy-pasted the code itself, but I do think I've actually tested the function body. Anyway, fixed the function + tested it nowMaize
I notice your solution recursively calls compareObjects when it encounters an object but it doesn't do this for the values in an array. Is that intentional?Diddle
@pomo: Don't think that was intentional (It's a 4.5 year old answer). Probably forgot to call it recursively for array elements, or might have had something to do with the original question or a comment somewhereMaize
Seeing this function I am surprised that someone being able to make one does not know how to correctly name things. What does compareObjects mean? What does it return? Is it boolean? Is it object made of different keys? If it returns true does it mean that objects are same or not? ... Also what is o? What is p? That is just terrible.Hollyanne
@Michal: For someone taking the time to criticise a 5-year-old snippet of code, it's remarkable you can't work out what return true and return false means (yes, function returns boolean, true for equal, false otherwise). o and p are clearly the arguments which, given the function is called compareObjects, are probably objects (hence o). Either way: yeah, the name isn't great, the style isn't either... then again: it's 5 years old... if I were still doing JS, I would write it in a completely different way using ES6/ES2016, perhaps name it objectEquals or whateverMaize
It is funny how when I ask some question or even when I give helpful answer everyone is like "what about "sql injection" or something else like "that is not ready for production" (even if it actually is supposed to be simple example) I also get a ton of negative rating. But when I decide to criticize someone they are all like "that is old no one cares anymore..." Function returning boolean should start with 'is' or with 'should' or have some similar prefix. Giving basically anything 1 letter variable name is just being lazy.Hollyanne
Other than that thank you for the example. People referencing lodash are even worse... :DHollyanne
H
3

As an underscore mixin:

in coffee-script:

_.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

And in 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;
  }
});
Horick answered 30/10, 2012 at 15:59 Comment(5)
Funkodebat, the iterator function that you passed to underscore each has inconsistent return points: if (prop1 === prop2) { return; /* void */ } if (!_.deepEquals(prop1, prop2)) { return _fail(); /* false */ }Clermontferrand
And what is the purpose of returning from the iterator? As I understood, _.each doesn't break the loop (if this is what you wanted)..Clermontferrand
Or is it just a semi-automatic conversion from coffeescript, where every statement has a return value, so when calling "_fail() unless _.deepEquals prop1, prop2" it converts this to "return _fail()" ?Clermontferrand
yea cofeescript just returns every last line of functionsHorick
Is there a difference to underscore's _.isEqual()? See underscorejs.org/#isEqual say it "Performs an optimized deep comparison between the two objects, to determine if they should be considered equal."Philippians

© 2022 - 2024 — McMap. All rights reserved.