This is a need I've run into more than once, yet there doesn't seem to be an elegant off-the-shelf solution that can handle a wide range of use cases, including simple (flat) objects, linear (one-dimensional) arrays, arrays of objects, and complex objects (objects within objects).

So here's a [relatively] straightforward recursive object comparison function that features optional loose type comparison (strict by default) for those times when you're working with data from different sources, (e.g., which give you quoted numbers ("3" == 3) or empty strings ("" == null == undefined == 0 == "0")), but your task at hand doesn't distinguish among these semantic differences.

Note that performance on huge objects has not been tested, but should be acceptable. Of course, there's likely room for improvement ;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// compare two objects recursively
// doesn't rely on specifics of object creation or property order
// performs strict type comparison on values by default, unless loose is set to TRUE
 
// USAGE
 
// obj1 = {'key1': 1, 'key2': 2};
// obj2 = {'key2': 2, 'key1': 1};
// objectCompare(obj1, obj2); // returns true
 
// obj1 = {'a': 0, 'b': ''};
// obj2 = {'b': null}; // don't even declare an 'a' property, so it will be undefined - note that number of properties is ignored when doing loose comparison
// objectCompare(obj1, obj2, true); // returns true... for 'a', 0 == undefined, and for 'b', '' == null
 
// will even compare arrays:
// obj1 = [0, 1];
// obj2 = [null, '1'];
// objectCompare(obj1, obj2, true); // returns true
 
// or even arrays of objects:
// obj1 = [{'a': 1}, {'b': '0'}];
// obj2 = [{'a': '1'}, {'b': null}];
// objectCompare(obj1, obj2, true); // returns true
 
// note that arrays are compared by index, so out-of-sequence arrays should be sorted before comparing
 
function objectCompare(obj1, obj2, loose) {
 
  // if loose is NOT TRUE, first do a quick comparison of lengths
  // quick & dirty attempt to avoid comparing all property values on large or complex objects
  var count1 = 0, count2 = 0;
  if (!loose){
    for (i in obj1){
      count1++;
    }
    for (i in obj2){
      count2++;
    }
  }
 
  if (count1 != count2){ // lengths are different, so no need to compare values
    return false;
  }
  else { // lengths are the same (or loose is TRUE), so continue to compare individual property values
    for (i in obj1) {
      if (typeof obj1[i] == 'object' && typeof obj2[i] == 'object') { // both items are objects
        if (!objectCompare(obj1[i], obj2[i], loose)){ // see if objects match
          return false;
        }
      }
      else if (
        loose // loose comparison, 42 == "42", null == '' == undefined
        &&
        !(
          (obj1[i] || '') == (obj2[i] || '') // this will work in all cases EXCEPT ("0" ?= 0|null|undefined), which will evaluate to ("0" ?= ""), which is of course FALSE
          ||
          ( // if either value is a number, try to convert both to numbers and compare - this will work around above exception
            (typeof obj1[i] == 'number' || typeof obj2[i] == 'number')
            &&
            Number(obj1[i]) == Number(obj2[i])
          )
        )
      ){ // only one item is an object, or objects don't match
        return false;
      }
      else if (
        !loose // strict comparison, 42 != "42", null != '' != undefined
        &&
        !(obj1[i] === obj2[i])
      ){
        return false;
      }
    }
  }
 
  return true; // all tests passed, objects are equivalent
}

Leave a Reply