19

I want to be able to get a list of all differences between two JavaScript object graphs, with the property names and values where the deltas occur.

For what it is worth, these objects are usually retrieved from the server as JSON and typically are no more than a handful of layers deep (i.e. it may be an array of objects that themselves have data and then arrays with other data objects).

I want to not only see the changes to basic properties, but differences in the number of members of an array, etc. etc.

If I don't get an answer, I will probably end up writing this myself, but hope someone has already done this work or know of someone who has.


EDIT: These objects will typically be very close in structure to one another, so we are not talking about objects that are utterly different from one another, but may have 3 or 4 deltas.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Jason Bunting
  • 58,249
  • 14
  • 102
  • 93
  • Jason, thanks for your support. I'm nevertheless going to delete my answer. Obviously it is not what you where after, and the implementation is debatable top of that, so I guess there is no value keeping it. If you have a solution that suits your needs I'd be interested to see it here. – Tomalak Nov 05 '08 at 19:23
  • I'm disappointed that you decided to remove it, but respect your right to do so. I have simply learned that there are things that can be learned from others' solutions to problems, even if they don't solve the exact problem they were designed for. We can learn from just about anything. – Jason Bunting Nov 05 '08 at 19:43
  • I give really nice answer here: https://stackoverflow.com/a/71173966/1919821 – pery mimon Feb 18 '22 at 14:19

10 Answers10

21

Here is a partial, naïve solution to my problem - I will update this as I further develop it.

function findDifferences(objectA, objectB) {
   var propertyChanges = [];
   var objectGraphPath = ["this"];
   (function(a, b) {
      if(a.constructor == Array) {
         // BIG assumptions here: That both arrays are same length, that
         // the members of those arrays are _essentially_ the same, and 
         // that those array members are in the same order...
         for(var i = 0; i < a.length; i++) {
            objectGraphPath.push("[" + i.toString() + "]");
            arguments.callee(a[i], b[i]);
            objectGraphPath.pop();
         }
      } else if(a.constructor == Object || (a.constructor != Number && 
                a.constructor != String && a.constructor != Date && 
                a.constructor != RegExp && a.constructor != Function &&
                a.constructor != Boolean)) {
         // we can safely assume that the objects have the 
         // same property lists, else why compare them?
         for(var property in a) {
            objectGraphPath.push(("." + property));
            if(a[property].constructor != Function) {
               arguments.callee(a[property], b[property]);
            }
            objectGraphPath.pop();
         }
      } else if(a.constructor != Function) { // filter out functions
         if(a != b) {
            propertyChanges.push({ "Property": objectGraphPath.join(""), "ObjectA": a, "ObjectB": b });
         }
      }
   })(objectA, objectB);
   return propertyChanges;
}

And here is a sample of how it would be used and the data it would provide (please excuse the long example, but I want to use something relatively non-trivial):

var person1 = { 
   FirstName : "John", 
   LastName : "Doh", 
   Age : 30, 
   EMailAddresses : [
      "john.doe@gmail.com", 
      "jd@initials.com"
   ], 
   Children : [ 
      { 
         FirstName : "Sara", 
         LastName : "Doe", 
         Age : 2 
      }, { 
         FirstName : "Beth", 
         LastName : "Doe", 
         Age : 5 
      } 
   ] 
};

var person2 = { 
   FirstName : "John", 
   LastName : "Doe", 
   Age : 33, 
   EMailAddresses : [
      "john.doe@gmail.com", 
      "jdoe@hotmail.com"
   ], 
   Children : [ 
      { 
         FirstName : "Sara", 
         LastName : "Doe", 
         Age : 3 
      }, { 
         FirstName : "Bethany", 
         LastName : "Doe", 
         Age : 5 
      } 
   ] 
};

var differences = findDifferences(person1, person2);

At this point, here is what the differences array would look like if you serialized it to JSON:

[
   {
      "Property":"this.LastName", 
      "ObjectA":"Doh", 
      "ObjectB":"Doe"
   }, {
      "Property":"this.Age", 
      "ObjectA":30, 
      "ObjectB":33
   }, {
      "Property":"this.EMailAddresses[1]", 
      "ObjectA":"jd@initials.com", 
      "ObjectB":"jdoe@hotmail.com"
   }, {
      "Property":"this.Children[0].Age", 
      "ObjectA":2, 
      "ObjectB":3
   }, {
      "Property":"this.Children[1].FirstName", 
      "ObjectA":"Beth", 
      "ObjectB":"Bethany"
   }
]

The this in the Property value refers to the root of the object that was compared. So, this solution is not yet exactly what I need, but it is pretty darn close.

Hope this is useful to someone out there, and if you have any suggestions for improvement, I am all-ears; I wrote this very late last night (i.e. early this morning) and there may be things I am completely overlooking.

Thanks.

Jason Bunting
  • 58,249
  • 14
  • 102
  • 93
  • This works great thanks, though I want to build up an object of differences – Dominic Jun 23 '15 at 15:38
  • 3
    Guys, please make attention on that: in ES5 arguments.callee is forbidden in strict mode. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments/callee – BotanMan Nov 20 '15 at 08:53
16

After reviewing the existing answers, I noticed that the https://github.com/flitbit/diff library was not yet listed as a solution.

From my research, this library seems to be the best in terms of active development, contributions and forks for solving the challenge of diffing objects. This is very handy for creating a diff on the server side and passing the client only the changed bits.

ryanm
  • 2,239
  • 21
  • 31
  • 1
    I changed my official answer from my own, to yours, since this is such an old question, and the world of JavaScript has changed significantly since I wrote it 7 years ago! :) – Jason Bunting Nov 10 '15 at 20:32
  • 2
    Yes, the world of JS has changed wildly. I never thought I'd be programming a backend in JavaScript! Thanks, @JasonBunting – ryanm Nov 10 '15 at 21:44
8

There is an objectDiff library which allows you to do that. On its demo page you can see a difference between two javascript objects.

fluffy
  • 5,212
  • 2
  • 37
  • 67
anton_byrna
  • 2,477
  • 1
  • 18
  • 31
7

You can also try rus-diff https://github.com/mirek/node-rus-diff which generates MongoDB compatible (rename/unset/set) diff.

For your example objects:

var person1 = {
  FirstName: "John",
  LastName: "Doh",
  Age: 30,
  EMailAddresses: ["john.doe@gmail.com", "jd@initials.com"],
  Children: [
    {
      FirstName: "Sara",
      LastName: "Doe",
      Age: 2
    }, {
      FirstName: "Beth",
      LastName: "Doe",
      Age: 5
    }
  ]
};

var person2 = {
  FirstName: "John",
  LastName: "Doe",
  Age: 33,
  EMailAddresses: ["john.doe@gmail.com", "jdoe@hotmail.com"],
  Children: [
    {
      FirstName: "Sara",
      LastName: "Doe",
      Age: 3
    }, {
      FirstName: "Bethany",
      LastName: "Doe",
      Age: 5
    }
  ]
};

var rusDiff = require('rus-diff').rusDiff

console.log(rusDiff(person1, person2))

It generates a list of sets:

{ '$set': 
   { 'Age': 33,
     'Children.0.Age': 3,
     'Children.1.FirstName': 'Bethany',
     'EMailAddresses.1': 'jdoe@hotmail.com',
     'LastName': 'Doe' } }
Mirek Rusin
  • 18,820
  • 3
  • 43
  • 36
5

Solution 1

Use JSON.stringify(obj) to get a string representation of the objects you want to compare. Save the string to a file. Use any diff viewer to compare the text files.

Note: JSON.stringify will ignore properties that point to function definitions.

Solution 2

This could do what you want with some modification, it is a modified version of the function _.isEqual (http://documentcloud.github.com/underscore/). Please feel free to suggest any modifications! I wrote it to figure out where the first difference between two objects occur.

// Given two objects find the first key or value not matching, algorithm is a
// inspired by of _.isEqual.
function diffObjects(a, b) {
  console.info("---> diffObjects", {"a": a, "b": b});
  // Check object identity.
  if (a === b) return true;
  // Different types?
  var atype = typeof(a), btype = typeof(b);
  if (atype != btype) {
    console.info("Type mismatch:", {"a": a, "b": b});
    return false;
  };
  // Basic equality test (watch out for coercions).
  if (a == b) return true;
  // One is falsy and the other truthy.
  if ((!a && b) || (a && !b)) {
    console.info("One is falsy and the other truthy:", {"a": a, "b": b});
    return false;
  }
  // Unwrap any wrapped objects.
  if (a._chain) a = a._wrapped;
  if (b._chain) b = b._wrapped;
  // One of them implements an isEqual()?
  if (a.isEqual) return a.isEqual(b);
  // Check dates' integer values.
  if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
  // Both are NaN?
  if (_.isNaN(a) && _.isNaN(b)) {
    console.info("Both are NaN?:", {"a": a, "b": b});
    return false;
  }
  // Compare regular expressions.
  if (_.isRegExp(a) && _.isRegExp(b))
    return a.source     === b.source &&
           a.global     === b.global &&
           a.ignoreCase === b.ignoreCase &&
           a.multiline  === b.multiline;
  // If a is not an object by this point, we can't handle it.
  if (atype !== 'object') {
    console.info("a is not an object:", {"a": a});
    return false;
  }
  // Check for different array lengths before comparing contents.
  if (a.length && (a.length !== b.length)) {
    console.info("Arrays are of different length:", {"a": a, "b": b});
    return false;
  }
  // Nothing else worked, deep compare the contents.
  var aKeys = _.keys(a), bKeys = _.keys(b);
  // Different object sizes?
  if (aKeys.length != bKeys.length) {
    console.info("Different object sizes:", {"a": a, "b": b});
    return false;
  }
  // Recursive comparison of contents.
  for (var key in a) if (!(key in b) || !diffObjects(a[key], b[key])) return false;
  return true;
};
mozey
  • 2,222
  • 2
  • 27
  • 34
3

This script has a NPM version as well, if you are using NodeJS. https://github.com/NV/objectDiff.js

Rejoice.

astone26
  • 1,222
  • 11
  • 16
1

I recently wrote a module to do this, because I wasn't satisfied with the numerous diffing modules I found (I listed a bunch of the most popular modules and why they weren't acceptable in the readme of my module). Its called odiff: https://github.com/Tixit/odiff . Here's an example:

var a = [{a:1,b:2,c:3},              {x:1,y: 2, z:3},              {w:9,q:8,r:7}]
var b = [{a:1,b:2,c:3},{t:4,y:5,u:6},{x:1,y:'3',z:3},{t:9,y:9,u:9},{w:9,q:8,r:7}]

var diffs = odiff(a,b)

/* diffs now contains:
[{type: 'add', path:[], index: 2, vals: [{t:9,y:9,u:9}]},
 {type: 'set', path:[1,'y'], val: '3'},
 {type: 'add', path:[], index: 1, vals: [{t:4,y:5,u:6}]}
]
*/
B T
  • 57,525
  • 34
  • 189
  • 207
1

None of the libraries I found were sufficient so I wrote my own AngularJS factory. It compares the objects in both ways and returns only the difference within the same structure.

/**
 * Diff
 * Original author: Danny Coulombe
 * Creation date: 2016-05-18
 * 
 * Work with objects to find their differences.
 */
controllers.factory('diff', [function() {

    var factory = {

        /**
         * Compare the original object with the modified one and return their differences.
         * 
         * @param original: Object
         * @param modified: Object
         * 
         * @return Object
         */
        getDifferences: function(original, modified) {

            var type = modified.constructor === Array ? [] : {};
            var result = angular.copy(type);
            var comparisons = [[original, modified, 1], [modified, original, 0]];

            comparisons.forEach(function(comparison) {

                angular.forEach(comparison[0], function(value, key) {

                    if(result[key] === undefined) {

                        if(comparison[1][key] !== undefined && value !==    null && comparison[1][key] !== null && [Object, Array].indexOf(comparison[1][key].constructor) !== -1) {

                            result[key] = factory.getDifferences(value, comparison[1][key]);
                        }
                        else if(comparison[1][key] !== value) {

                            result[key] = comparison[comparison[2]][key];
                        }

                        if(angular.equals(type, result[key])
                        || result[key] === undefined
                        || (
                            comparison[0][key] !== undefined
                            && result[key] !== null
                            && comparison[0][key] !== null
                            && comparison[0][key].length === comparison[1][key].length
                            && result[key].length === 0
                        )) {
                            delete result[key];
                        }
                    }
                });
            });

            return result;
        }
    };

    return factory;
}]);
Chololoco
  • 3,140
  • 1
  • 21
  • 24
  • I get "source is undefined" when I run this. Is "original" supposed to be "source"? Also, where is "fromObjects" defined? – adam0101 May 19 '16 at 15:00
  • Sorry, I changed some variable names while writing this answer. "source" was "original" and "fromObjects" was "getDifferences". I updated the script above. – Chololoco May 20 '16 at 13:21
1

var d = { 
   FirstName : "John", 
   LastName : "Doh", 
   Age : 30, 
   EMailAddresses : [
      "john.doe@gmail.com", 
      "jd@initials.com"
   ], 
   Children : [ 
      { 
         FirstName : "Sara", 
         LastName : "Doe", 
         Age : 2 
      }, { 
         FirstName : "Beth", 
         LastName : "Doe", 
         Age : 5 
      } 
   ] 
};

var f = { 
   FirstName : "John", 
   LastName : "Doe", 
   Age : 33, 
   EMailAddresses : [
      "john.doe@gmail.com", 
      "jdoe@hotmail.com"
   ], 
   Children : [ 
      { 
         FirstName : "Sara", 
         LastName : "Doe", 
         Age : 3 
      }, { 
         FirstName : "Bethany", 
         LastName : "Doe", 
         Age : 5 
      } 
   ] 
};

resultobj = []
function comp_obj(t1,t2){
 flag = 1;
 key1_arr = Object.keys(t1)
 key2_arr = Object.keys(t2)
 if(key1_arr.length == key2_arr.length){
  for(key1 in t1){
   ty1    = Object.prototype.toString.call(t1[key1])
   ty2    = Object.prototype.toString.call(t2[key1])
   if(ty1 == ty2) {
    if(ty1 == '[object String]' || ty1 == '[object Number]' ){
     if(t2[key1] != t1[key1]){
      flag = 0;
      break;
     } 
    }else if(ty1 == '[object Array]'){
     var result = comp_arr(t1[key1],t2[key1]);
     console.log(ty1,ty2)
     if(!result)
      flag = 0;
    }else if(ty1  == '[object Object]'){
     var result = comp_obj(t1[key1],t2[key1])
     if(!result)
      flag = 0;
      
    }
   }else{
    flag = 0;
    break; 
   }

  }
 }else{
  flag = 0;
 }
 if(flag)
  return true
 else
  return false;
}
function comp_arr(a,b){
 flag = 1;
 if(a.length == b.length ){
  for(var i=0,l=a.length;i<l;i++){
   type1    = Object.prototype.toString.call(a[i])
   type2    = Object.prototype.toString.call(b[i])
   if(type1 == type2) {
    if(type1 == '[object String]' || type1 == '[object Number]' ){
     if( a[i] != b[i]){
      flag = 0;
      break;
     } 
    }else if(type1 == '[object Array]'){
     var result = comp_arr(a[i],b[i]);
     if(!result)
      flag = 0;
    }else if(type1  == '[object Object]'){
     var result = comp_obj(a[i],b[i])
     if(!result)
      flag = 0;
    } 
   }else{
    flag = 0;
    break; 
   }
  }
 }else
   flag = 0;
 if(flag)
  return true
 else
  return false;
}
function create(t,attr,parent_node,innerdata){
 var dom = document.createElement(t)
 for(key in attr){
  dom.setAttribute(key,attr[key])
 }
 dom.innerHTML = innerdata;
 parent_node.appendChild(dom)
 return dom;
}
window.onload = function () {
 for(key in f){
  if(!(key in d)) 
   resultobj.push({'Property_name':key,'f_value':JSON.stringify(f[key]),'d_value':JSON.stringify(d[key])})
  type1    = Object.prototype.toString.call(f[key])
  type2    = Object.prototype.toString.call(d[key])
  if(type1 == type2){
   if(type1 == '[object String]' || type1 == '[object Number]' ){
    if(f[key] != d[key])
     resultobj.push({'Property_name':key,'f_value':JSON.stringify(f[key]),'d_value':JSON.stringify(d[key])})
   }else if(type1 == '[object Array]'){
    var result = comp_arr(f[key],d[key]);
    if(!result)
     resultobj.push({'Property_name':key,'f_value':JSON.stringify(f[key]),'d_value':JSON.stringify(d[key])})
   }else if(type1 == '[object Object]'){
    var result = comp_obj(f[key],d[key]) 
    if(!result)
     resultobj.push({'Property_name':key,'f_value':JSON.stringify(f[key]),'d_value':JSON.stringify(d[key])})
   }
  }else 
   resultobj.push({'Property_name':key,'f_value':JSON.stringify(f[key]),'d_value':JSON.stringify(d[key])})
 }
 var tb = document.getElementById('diff');
 var s1 = document.getElementById('source1');
 var s2 = document.getElementById('source2');
 s1.innerHTML = 'Object 1 :'+ JSON.stringify(f)
 s2.innerHTML = 'Object 2 :'+JSON.stringify(d)
 resultobj.forEach(function(data,i){
   tr_dom = create('tr',{},tb,'')
   no = create('td',{},tr_dom,i+1)
   Object.keys(data).forEach(function(tr){
    td_dom = create('td',{},tr_dom,data[tr])
   })
 })
}
  <html>
    <body>
      <p id="source1"> </p>
      <p id="source2"> </p>
      <p id="source7"> DIFFERENCE TABLE</p>
      <table border=''>
        <thead>
          <th>S.no</th>
          <th>Name Of the Key</th>
          <th>Object1 Value</th>
          <th>Object2 Value</th>
        </thead>
        <tbody id="diff">

        </tbody>
      </table>
    </body>
</html>
Mickael Lherminez
  • 679
  • 1
  • 10
  • 29
-2

you can do'it with filter and indexOf

var first = [ 1, 2, 3, 4, 5 ];
var second = [ 4, 5, 6 ];

var difference = first.filter(x => second.indexOf(x) === -1);
console.log(difference);