335

I have two objects: oldObj and newObj.

The data in oldObj was used to populate a form and newObj is the result of the user changing data in this form and submitting it.

Both objects are deep, ie. they have properties that are objects or arrays of objects etc - they can be n levels deep, thus the diff algorithm needs to be recursive.

Now I need to not just figure out what was changed (as in added/updated/deleted) from oldObj to newObj, but also how to best represent it.

So far my thoughts was to just build a genericDeepDiffBetweenObjects method that would return an object on the form {add:{...},upd:{...},del:{...}} but then I thought: somebody else must have needed this before.

So... does anyone know of a library or a piece of code that will do this and maybe have an even better way of representing the difference (in a way that is still JSON serializable)?

Update:

I have thought of a better way to represent the updated data, by using the same object structure as newObj, but turning all property values into objects on the form:

{type: '<update|create|delete>', data: <propertyValue>}

So if newObj.prop1 = 'new value' and oldObj.prop1 = 'old value' it would set returnObj.prop1 = {type: 'update', data: 'new value'}

Update 2:

It gets truely hairy when we get to properties that are arrays, since the array [1,2,3] should be counted as equal to [2,3,1], which is simple enough for arrays of value based types like string, int & bool, but gets really difficult to handle when it comes to arrays of reference types like objects and arrays.

Example arrays that should be found equal:

[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]

Not only is it quite complex to check for this type of deep value equality, but also to figure out a good way to represent the changes that might be.

Community
  • 1
  • 1
Martin Jespersen
  • 25,743
  • 8
  • 56
  • 68
  • 1
    possible duplicate of [Difference in JSON objects using Javascript/JQuery](http://stackoverflow.com/questions/1200562/difference-in-json-objects-using-javascript-jquery) – a'r Dec 20 '11 at 09:04
  • 6
    @a'r: It is not a duplicate of http://stackoverflow.com/questions/1200562/difference-in-json-objects-using-javascript-jquery - I know how to traverse the objects, I am looking for prior art since this is non trivial and will take real time to implement, and I'd rather use a library than make it from scratch. – Martin Jespersen Dec 20 '11 at 09:20
  • 2
    Do you really need diff of objects, is that newObj generated from server on form submit response? Because if you don't have "server updates" of a object you could simplify your problem by attaching appropriate event listeners and upon user interaction (object change) you could update/generate wanted change list. – sbgoran Dec 21 '11 at 10:35
  • 1
    @sbgoran: `newObj` is generated by js code reading values from a form in the DOM. There are several ways to keep state and do this much easier, but I'd like to keep it stateless as an exercise. Also I am looking for prior art to see how others might have tackled this, if indeed anyone has. – Martin Jespersen Dec 21 '11 at 11:02
  • -1 for the rambling sequence of 'Update' sections; simply editing the main body of your question rather than tacking on postscripts would've left this in a state easier for future visitors to read. – Mark Amery May 10 '14 at 18:05
  • A good answer also found here: [How can you map the differences between JavaScript objects?](http://codereview.stackexchange.com/questions/11412/how-can-you-map-the-differences-between-javascript-objects/11580#11580) – laggingreflex Jun 23 '14 at 08:49
  • 5
    here's a very sophisticated library to diff/patch any pair of Javascript objects https://github.com/benjamine/jsondiffpatch you can see it live here: http://benjamine.github.io/jsondiffpatch/demo/index.html (disclaimer: I'm the author) – Benja Mar 02 '14 at 07:27
  • I needed a library that does 'deef diff' thing and i found this https://github.com/flitbit/diff very useful – user3870075 Jun 10 '19 at 21:04
  • if u need something to reorder the array regarding of the content do the following and it will be ordered the same if no difference `arr.sort((a,b) => JSON.stringify(a).localeCompare(JSON.stringify(b))) `, do this for the original one and the modified one – Mateus Martins May 06 '20 at 14:42

29 Answers29

214

I wrote a little class that is doing what you want, you can test it here.

Only thing that is different from your proposal is that I don't consider

[1,[{c: 1},2,3],{a:'hey'}]

and

[{a:'hey'},1,[3,{c: 1},2]]

to be same, because I think that arrays are not equal if order of their elements is not same. Of course this can be changed if needed. Also this code can be further enhanced to take function as argument that will be used to format diff object in arbitrary way based on passed primitive values (now this job is done by "compareValues" method).

var deepDiffMapper = function () {
  return {
    VALUE_CREATED: 'created',
    VALUE_UPDATED: 'updated',
    VALUE_DELETED: 'deleted',
    VALUE_UNCHANGED: 'unchanged',
    map: function(obj1, obj2) {
      if (this.isFunction(obj1) || this.isFunction(obj2)) {
        throw 'Invalid argument. Function given, object expected.';
      }
      if (this.isValue(obj1) || this.isValue(obj2)) {
        return {
          type: this.compareValues(obj1, obj2),
          data: obj1 === undefined ? obj2 : obj1
        };
      }

      var diff = {};
      for (var key in obj1) {
        if (this.isFunction(obj1[key])) {
          continue;
        }

        var value2 = undefined;
        if (obj2[key] !== undefined) {
          value2 = obj2[key];
        }

        diff[key] = this.map(obj1[key], value2);
      }
      for (var key in obj2) {
        if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
          continue;
        }

        diff[key] = this.map(undefined, obj2[key]);
      }

      return diff;

    },
    compareValues: function (value1, value2) {
      if (value1 === value2) {
        return this.VALUE_UNCHANGED;
      }
      if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
        return this.VALUE_UNCHANGED;
      }
      if (value1 === undefined) {
        return this.VALUE_CREATED;
      }
      if (value2 === undefined) {
        return this.VALUE_DELETED;
      }
      return this.VALUE_UPDATED;
    },
    isFunction: function (x) {
      return Object.prototype.toString.call(x) === '[object Function]';
    },
    isArray: function (x) {
      return Object.prototype.toString.call(x) === '[object Array]';
    },
    isDate: function (x) {
      return Object.prototype.toString.call(x) === '[object Date]';
    },
    isObject: function (x) {
      return Object.prototype.toString.call(x) === '[object Object]';
    },
    isValue: function (x) {
      return !this.isObject(x) && !this.isArray(x);
    }
  }
}();


var result = deepDiffMapper.map({
  a: 'i am unchanged',
  b: 'i am deleted',
  e: {
    a: 1,
    b: false,
    c: null
  },
  f: [1, {
    a: 'same',
    b: [{
      a: 'same'
    }, {
      d: 'delete'
    }]
  }],
  g: new Date('2017.11.25')
}, {
  a: 'i am unchanged',
  c: 'i am created',
  e: {
    a: '1',
    b: '',
    d: 'created'
  },
  f: [{
    a: 'same',
    b: [{
      a: 'same'
    }, {
      c: 'create'
    }]
  }, 1],
  g: new Date('2017.11.25')
});
console.log(result);
Deepak Dixit
  • 1,510
  • 15
  • 24
sbgoran
  • 3,451
  • 2
  • 19
  • 30
  • 3
    +1 It's not a bad piece of code. There is a bug however (check this example out: http://jsfiddle.net/kySNu/3/ `c` is created as `undefined` but should be the string `'i am created'`), and besides it doesn't do what I need since it is lacking the deep array value compare which is the most crucial (and complex/difficult) part. As a side note the construct `'array' != typeof(obj)` is useless since arrays are objects that are instances of arrays. – Martin Jespersen Dec 21 '11 at 21:43
  • 1
    I updated code, but I'm not sure what value you want in resulting object, right now code is returning value from first object and if it doesn't exist value from second one will be set as data. – sbgoran Dec 21 '11 at 22:18
  • 1
    And how do you mean "lacking the deep array value compare" for arrays you'll get for each index that `{type: ..., data:..}` object. What is missing is searching value from first array in second, but as I mentioned in my answer I don't think that arrays are equal if order of their values are not equal (`[1, 2, 3] is not equal to [3, 2, 1]` in my opinion). – sbgoran Dec 21 '11 at 22:32
  • 1
    Despite your opinion, the data is in fact equal, as in - the data is in a database and an array reflects a table (which might or might not have subtables etc) – Martin Jespersen Dec 22 '11 at 07:32
  • 8
    @MartinJespersen OK, how would you generically treat this arrays then: `[{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]`. Now is first object in first array updated with "value1" or "value2". And this is simple example, it could get much complicated with deep nesting. If you want/need deep nesting comparison regardless of key position don't create arrays of objects, create objects with nested objects like for previous example: `{inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}`. – sbgoran Dec 22 '11 at 08:27
  • 3
    I agree with you last point of view - the original data structure should be changed to something that is easier to do an actual diff on. Congrats, you nailed it :) – Martin Jespersen Mar 02 '14 at 18:39
  • 1
    There is a minor flaw when it comes to boolean values. If `obj1 = true` and `obj2 = false`, the assignment `data: obj1 || obj2` will yield `true` even though the type is `updated`. This can be solved with a more strict check `data: obj2 === undefined ? obj1 : obj2`. – PerfectPixel Aug 31 '16 at 15:20
  • @PerfectPixel Thanks, I updated the code with more strict check, but I used `obj1` value as a default to stay inline with previous logic. – sbgoran Sep 01 '16 at 20:43
  • 1
    @sbgoran thanks for the update. However using `obj1` as a default will yield an incorrect diff. I modified your fiddle an added the property `g` which is updated from `true` to `false`. Expected would be data `false` and type `changed`. However `obj1 !== undefined`, therefore, obj1 is used and data is `true` not `false`. Here is the fiddle: http://jsfiddle.net/kySNu/275/ :-) – PerfectPixel Sep 02 '16 at 08:26
  • @PerfectPixel You are probably right, but as I mentioned in [my previous comment](http://stackoverflow.com/questions/8572826/generic-deep-diff-between-two-objects/8596559?noredirect=1#comment10665575_8596559) I done it other way around first time, not knowing what is desired result. I just wanted to stay inline with initial logic. This, of course, can be modified as needed. – sbgoran Sep 13 '16 at 09:19
  • 1
    Unfortunately this code does not work with javascript date objects. Those are always flagged as being updated. – Sander van den Akker Dec 21 '16 at 16:30
  • @SandervandenAkker You are right, code updated, thanks. – sbgoran Feb 16 '17 at 16:17
  • @jlee It works fine with empty arrays (on my machine :)). Could you provide full `deepDiffMapper.map` method call example, maybe I can do something about it (when I get some free time). – sbgoran Feb 16 '17 at 16:20
  • 3
    you know an answer is good when it gets copied and modified a lot – shieldgenerator7 Jun 13 '20 at 23:38
  • Mh from my tests there's a problem in this. If I compare an old and new object, and the new one as a nested property changed and another one added, the "changed" diff is reversed. It shows the old value. If I swap the input objects, the added value is "deleted" (correct) and the "chanced" diff now shows the new value. Just me? – ProblemsOfSumit Jan 16 '21 at 17:34
  • @sbgoran - is it possible return array of objects instead of object with objects? I compare two arrays of object and I would like to get the same result (not object) ... like attached image. [link](https://ibb.co/3WWhxB6) thx – Tomáš Trúsik Oct 21 '21 at 09:16
  • Just tried it on some large objects and got: `Uncaught RangeError: Maximum call stack size exceeded` – JohnK May 18 '23 at 20:56
116

Using Underscore, a simple diff:

var o1 = {a: 1, b: 2, c: 2},
    o2 = {a: 2, b: 1, c: 2};

_.omit(o1, function(v,k) { return o2[k] === v; })

Results in the parts of o1 that correspond but with different values in o2:

{a: 1, b: 2}

It'd be different for a deep diff:

function diff(a,b) {
    var r = {};
    _.each(a, function(v,k) {
        if(b[k] === v) return;
        // but what if it returns an empty object? still attach?
        r[k] = _.isObject(v)
                ? _.diff(v, b[k])
                : v
            ;
        });
    return r;
}

As pointed out by @Juhana in the comments, the above is only a diff a-->b and not reversible (meaning extra properties in b would be ignored). Use instead a-->b-->a:

(function(_) {
  function deepDiff(a, b, r) {
    _.each(a, function(v, k) {
      // already checked this or equal...
      if (r.hasOwnProperty(k) || b[k] === v) return;
      // but what if it returns an empty object? still attach?
      r[k] = _.isObject(v) ? _.diff(v, b[k]) : v;
    });
  }

  /* the function */
  _.mixin({
    diff: function(a, b) {
      var r = {};
      deepDiff(a, b, r);
      deepDiff(b, a, r);
      return r;
    }
  });
})(_.noConflict());

See http://jsfiddle.net/drzaus/9g5qoxwj/ for full example+tests+mixins

drzaus
  • 24,171
  • 16
  • 142
  • 201
  • Not sure why you got downvoted, this was sufficient as you provided a shallow, simple example as well as a more complex deep function. – Seiyria Nov 01 '14 at 01:03
  • 2
    @Seiyria haters gonna hate, I guess... I did both because I originally thought `omit` would be a deep diff, but was wrong, so included also for comparison. – drzaus Nov 03 '14 at 19:33
  • 1
    Nice solution. I would suggest to change `r[k] = ... : v` in `r[k] = ... : {'a':v, 'b':b[k] }`, this way you can see two values. – guyaloni Feb 03 '15 at 17:59
  • 2
    Both of these return a false negative when the objects are otherwise identical but the second one has more elements, e.g. `{a:1, b:2}` and `{a:1, b:2, c:3}`. – JJJ Jan 19 '16 at 20:53
  • @Juhana - oh I see what you mean http://jsfiddle.net/drzaus/9g5qoxwj/42/ -- `diff(a, c)` vs `diff(c, a)`. This would fix it for the deep diff http://jsfiddle.net/drzaus/9g5qoxwj/44/ – drzaus Jan 20 '16 at 16:03
  • I have improved a bit the proposed second solution, in order first to make it work and also to remove empty objects. Not perfect but usable in simple cases : `var objectDiff = function diff(a,b) { var r = {}; _.each(a, function(v,k) { if(b[k] === v) return; val = _.isObject(v) ? objectDiff(v, b[k]) : v; if (!_.isObject(val) || _.keys(val).length>0) r[k]=val; }); return r; } Object.diff = function( x, y ) { return objectDiff(x,y); }` – Cédric NICOLAS Jul 11 '16 at 17:01
  • 8
    It should be `_.omitBy` instead of `_.omit`. – jpsecher Feb 23 '17 at 12:11
  • does not account for Date Objects. see diff http://jsfiddle.net/cajqbz56/16/ – Jackstine Jan 26 '22 at 20:48
71

I'd like to offer an ES6 solution...This is a one-way diff, meaning that it will return keys/values from o2 that are not identical to their counterparts in o1:

let o1 = {
  one: 1,
  two: 2,
  three: 3
}

let o2 = {
  two: 2,
  three: 3,
  four: 4
}

let diff = Object.keys(o2).reduce((diff, key) => {
  if (o1[key] === o2[key]) return diff
  return {
    ...diff,
    [key]: o2[key]
  }
}, {})
senornestor
  • 4,075
  • 2
  • 33
  • 33
  • Is the code complete? I'm getting `Uncaught SyntaxError: Unexpected token ...` – Sean H May 11 '17 at 10:33
  • It works for me in the console of Chrome 58. The `let` keyword is ES6 and the `...` object spread operator is ES future, so you need to run this in an environment that supports them. – senornestor May 12 '17 at 19:25
  • May miss a little bit of explanation, but at least this one works like a charm, +1! – Ulysse BN Jul 18 '17 at 16:09
  • 6
    I like the solution but it has one problem, if the object is deeper than one level, it will return all the values in the changed nested objects - or at least that's whats happening for me. – Spurious Aug 07 '17 at 14:23
  • 4
    Yup, this is not recursive @Spurious – Nemesarial Aug 14 '17 at 11:12
  • great solution! Updated with ES6, just what I was looking for. Make it recursive is quite straight forward. Thanks! – SrAxi Nov 13 '17 at 09:28
  • 5
    Just bear in mind that with this solution, for each element in the object you get an entirely new object built with all existing elements copied into it just to add one item to the array. For small objects it's fine, but it will slow down exponentially for larger objects. – Malvineous Feb 20 '18 at 03:42
  • Does the return { ...diff, [key]: o2[key] } diff attribute not need to be named? – Ben Taliadoros Oct 16 '18 at 10:01
  • Ben, the `[key]` is a "computed property name" meaning that the value of `key` will be used as the property name in the object. – senornestor Oct 17 '18 at 04:54
  • This also doesn't work if the elements inside are objects `{a: '1', b: '2'} === {a: '1', b: '2'}` is false because they are different objects you need to add a check like `if (typeof o1[key] === "object" && typeof o2[key] === "object") { return JSON.stringify(o1[key]) === JSON.stringify(o2[key]); }` You also need to concat the keys of both objects to get a full diff `Object.keys(o1).concat(Object.keys(o2)).reduce(...)` – Tofandel Jun 11 '19 at 19:32
  • @SrAxi, how to Make it recursive? – Michael Freidgeim Apr 23 '20 at 13:42
  • @MichaelFreidgeim Transform that into a function that accepts 2 objects. Inside it, whenever you iterate, call itself passing the object that you want to compare to and current object in iteration. – SrAxi Apr 23 '20 at 18:02
  • 5
    [Object.fromEntries()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) makes this solution even cleaner `Object.fromEntries(Object.entries(o2).filter(([k, v]) => o1[k] !== v))` – Jamby May 26 '22 at 12:28
29

Here is a JavaScript library which can be used for finding diff between two JavaScript objects:

Github URL: https://github.com/cosmicanant/recursive-diff

Npmjs url: https://www.npmjs.com/package/recursive-diff

recursive-diff library can be used in the browser as well as in a Node.js based server side application. For browser, it can be used as below:

<script type="text" src="https://unpkg.com/recursive-diff@latest/dist/recursive-diff.min.js"/>
<script type="text/javascript">
     const ob1 = {a:1, b: [2,3]};
     const ob2 = {a:2, b: [3,3,1]};
     const delta = recursiveDiff.getDiff(ob1,ob2); 
     /* console.log(delta) will dump following data 
     [
         {path: ['a'], op: 'update', val: 2}
         {path: ['b', '0'], op: 'update',val: 3},
         {path: ['b',2], op: 'add', val: 1 },
     ]
      */
     const ob3 = recursiveDiff.applyDiff(ob1, delta); //expect ob3 is deep equal to ob2
 </script>

Whereas in a node.js based application it can be used as below:

const diff = require('recursive-diff');
const ob1 = {a: 1}, ob2: {b:2};
const diff = diff.getDiff(ob1, ob2);
Anant
  • 3,047
  • 2
  • 27
  • 33
  • This will not account for changes in Date properties, for example. – trollkotze Nov 17 '17 at 10:03
  • 1
    date support is added – Anant Nov 15 '19 at 14:42
  • It doesn't look like this recognizes moved content. E.g. correctly idenfitying the diff between `{ a: { b: { c: '...' }}}` and `{ b: { c: '...' }, a: {}}` as "move `/a/b` to `/b`" – Mike 'Pomax' Kamermans Aug 13 '21 at 20:54
  • You may think move as a combination of delete and then insert. You can write your own algorithm to get this data from the computed diff. IMHO diff algorithm is simple when you compute diff while considering preserved order of objects/array, otherwise it can be way more complex. Same problem with array insert and delete, a single insert/delete can bloat the whole diff data. – Anant Aug 14 '21 at 04:33
26

Using Lodash:

_.mergeWith(oldObj, newObj, function (objectValue, sourceValue, key, object, source) {
    if ( !(_.isEqual(objectValue, sourceValue)) && (Object(objectValue) !== objectValue)) {
        console.log(key + "\n    Expected: " + sourceValue + "\n    Actual: " + objectValue);
    }
});

I don't use key/object/source but I left it in there if you need to access them. The object comparison just prevents the console from printing the differences to the console from the outermost element to the innermost element.

You can add some logic inside to handle arrays. Perhaps sort the arrays first. This is a very flexible solution.

EDIT

Changed from _.merge to _.mergeWith due to lodash update. Thanks Aviron for noticing the change.

toshiomagic
  • 1,335
  • 1
  • 12
  • 39
25
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

There is an npm module with over 500k weekly downloads: https://www.npmjs.com/package/deep-object-diff

I like the object like representation of the differences - especially it is easy to see the structure, when it is formated.

const diff = require("deep-object-diff").diff;

const lhs = {
  foo: {
    bar: {
      a: ['a', 'b'],
      b: 2,
      c: ['x', 'y'],
      e: 100 // deleted
    }
  },
  buzz: 'world'
};

const rhs = {
  foo: {
    bar: {
      a: ['a'], // index 1 ('b')  deleted
      b: 2, // unchanged
      c: ['x', 'y', 'z'], // 'z' added
      d: 'Hello, world!' // added
    }
  },
  buzz: 'fizz' // updated
};

console.log(diff(lhs, rhs)); // =>
/*
{
  foo: {
    bar: {
      a: {
        '1': undefined
      },
      c: {
        '2': 'z'
      },
      d: 'Hello, world!',
      e: undefined
    }
  },
  buzz: 'fizz'
}
*/
Felix Furtmayr
  • 371
  • 3
  • 4
20

Here is a solution that is:

  • TypeScript (but easily convertible to JavaScript)
  • have no lib dependencies
  • generic, and doesn't care about checking object types (aside the object type)
  • supports properties with value undefined
  • deep of not (default)

First we define the comparison result interface:

export interface ObjectDiff {
  added: {} | ObjectDiff;
  updated: {
    [propName: string]: Update | ObjectDiff;
  };
  removed: {} | ObjectDiff;
  unchanged: {} | ObjectDiff;
}

with the special case of change where we want to know what are old and new values:

export interface Update {
  oldValue: any;
  newValue: any;
}

Then we can provide the diff function which is merely two loops (with recursivity if deep is true):

export class ObjectUtils {
  /**
   * @return if obj is an Object, including an Array.
   */
  static isObject(obj: any) {
    return obj !== null && typeof obj === 'object';
  }

  /**
   * @param oldObj The previous Object or Array.
   * @param newObj The new Object or Array.
   * @param deep If the comparison must be performed deeper than 1st-level properties.
   * @return A difference summary between the two objects.
   */
  static diff(oldObj: {}, newObj: {}, deep = false): ObjectDiff {
    const added = {};
    const updated = {};
    const removed = {};
    const unchanged = {};
    for (const oldProp in oldObj) {
      if (oldObj.hasOwnProperty(oldProp)) {
        const newPropValue = newObj[oldProp];
        const oldPropValue = oldObj[oldProp];
        if (newObj.hasOwnProperty(oldProp)) {
          if (newPropValue === oldPropValue) {
            unchanged[oldProp] = oldPropValue;
          } else {
            updated[oldProp] = deep && this.isObject(oldPropValue) && this.isObject(newPropValue) ? this.diff(oldPropValue, newPropValue, deep) : {newValue: newPropValue};
          }
        } else {
          removed[oldProp] = oldPropValue;
        }
      }
    }
    for (const newProp in newObj) {
      if (newObj.hasOwnProperty(newProp)) {
        const oldPropValue = oldObj[newProp];
        const newPropValue = newObj[newProp];
        if (oldObj.hasOwnProperty(newProp)) {
          if (oldPropValue !== newPropValue) {
            if (!deep || !this.isObject(oldPropValue)) {
              updated[newProp].oldValue = oldPropValue;
            }
          }
        } else {
          added[newProp] = newPropValue;
        }
      }
    }
    return {added, updated, removed, unchanged};
  }
}

As an example, calling:

ObjectUtils.diff(
  {
    a: 'a', 
    b: 'b', 
    c: 'c', 
    arr: ['A', 'B'], 
    obj: {p1: 'p1', p2: 'p2'}
  },
  {
    b: 'x', 
    c: 'c', 
    arr: ['B', 'C'], 
    obj: {p2: 'p2', p3: 'p3'}, 
    d: 'd'
  },
);

will return:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {oldValue: ['A', 'B'], newValue: ['B', 'C']},
    obj: {oldValue: {p1: 'p1', p2: 'p2'}, newValue: {p2: 'p2', p3: 'p3'}}
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}

and calling the same with the deep third parameter will return:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {
      added: {},
      removed: {},
      unchanged: {},
      updated: {
        0: {oldValue: 'A', newValue: 'B'},
        1: {oldValue: 'B', newValue: 'C', }
      }
    },
    obj: {
      added: {p3: 'p3'},
      removed: {p1: 'p1'},
      unchanged: {p2: 'p2'},
      updated: {}
    }
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}
Jérôme Beau
  • 10,608
  • 5
  • 48
  • 52
15

I modified @sbgoran's answer so that the resulting diff object includes only the changed values, and omits values that were the same. In addition, it shows both the original value and the updated value.

var deepDiffMapper = function () {
    return {
        VALUE_CREATED: 'created',
        VALUE_UPDATED: 'updated',
        VALUE_DELETED: 'deleted',
        VALUE_UNCHANGED: '---',
        map: function (obj1, obj2) {
            if (this.isFunction(obj1) || this.isFunction(obj2)) {
                throw 'Invalid argument. Function given, object expected.';
            }
            if (this.isValue(obj1) || this.isValue(obj2)) {
                let returnObj = {
                    type: this.compareValues(obj1, obj2),
                    original: obj1,
                    updated: obj2,
                };
                if (returnObj.type != this.VALUE_UNCHANGED) {
                    return returnObj;
                }
                return undefined;
            }

            var diff = {};
            let foundKeys = {};
            for (var key in obj1) {
                if (this.isFunction(obj1[key])) {
                    continue;
                }

                var value2 = undefined;
                if (obj2[key] !== undefined) {
                    value2 = obj2[key];
                }

                let mapValue = this.map(obj1[key], value2);
                foundKeys[key] = true;
                if (mapValue) {
                    diff[key] = mapValue;
                }
            }
            for (var key in obj2) {
                if (this.isFunction(obj2[key]) || foundKeys[key] !== undefined) {
                    continue;
                }

                let mapValue = this.map(undefined, obj2[key]);
                if (mapValue) {
                    diff[key] = mapValue;
                }
            }

            //2020-06-13: object length code copied from https://stackoverflow.com/a/13190981/2336212
            if (Object.keys(diff).length > 0) {
                return diff;
            }
            return undefined;
        },
        compareValues: function (value1, value2) {
            if (value1 === value2) {
                return this.VALUE_UNCHANGED;
            }
            if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
                return this.VALUE_UNCHANGED;
            }
            if (value1 === undefined) {
                return this.VALUE_CREATED;
            }
            if (value2 === undefined) {
                return this.VALUE_DELETED;
            }
            return this.VALUE_UPDATED;
        },
        isFunction: function (x) {
            return Object.prototype.toString.call(x) === '[object Function]';
        },
        isArray: function (x) {
            return Object.prototype.toString.call(x) === '[object Array]';
        },
        isDate: function (x) {
            return Object.prototype.toString.call(x) === '[object Date]';
        },
        isObject: function (x) {
            return Object.prototype.toString.call(x) === '[object Object]';
        },
        isValue: function (x) {
            return !this.isObject(x) && !this.isArray(x);
        }
    }
}();
shieldgenerator7
  • 1,507
  • 17
  • 22
  • Ty, the original was too verbose. But i had to modify it further to also check for typed arrays... `isArray: x => Array.isArray(x) || ArrayBuffer.isView(x)` – Endless Feb 06 '21 at 02:33
  • This is a nice improvement, though worth mentioning that if the two objects match then it just returns `undefined` :) – slugmandrew Sep 29 '21 at 09:47
12

These days, there are quite a few modules available for this. I recently wrote a module to do this, because I wasn't satisfied with the numerous diffing modules I found. Its called odiff: https://github.com/Tixit/odiff . I also listed a bunch of the most popular modules and why they weren't acceptable in the readme of odiff, which you could take a look through if odiff doesn't have the properties you want. 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
9

I know I'm late to the party, but I needed something similar that the above answers didn't help.

I was using Angular's $watch function to detect changes in a variable. Not only did I need to know whether a property had changed on the variable, but I also wanted to make sure that the property that changed was not a temporary, calculated field. In other words, I wanted to ignore certain properties.

Here's the code:

function diff(obj1,obj2,exclude) {
        var r = {};
    
    if (!exclude)   exclude = [];
    
    for (var prop in obj1) {
      if (obj1.hasOwnProperty(prop) && prop != '__proto__') {
            if (exclude.indexOf(obj1[prop]) == -1) {

            // check if obj2 has prop
            if (!obj2.hasOwnProperty(prop)) r[prop] = obj1[prop];

            // check if prop is object and 
            // NOT a JavaScript engine object (i.e. __proto__), if so, recursive diff
            else if (obj1[prop] === Object(obj1[prop])) {
              var difference = diff(obj1[prop], obj2[prop]);
              if (Object.keys(difference).length > 0) r[prop] = difference;
            }

            // check if obj1 and obj2 are equal
            else if (obj1[prop] !== obj2[prop]) {
                if (obj1[prop] === undefined)
                r[prop] = 'undefined';
                if (obj1[prop] === null)
                r[prop] = null;
              else if (typeof obj1[prop] === 'function')
                r[prop] = 'function';
              else if (typeof obj1[prop] === 'object')  
                r[prop] = 'object';
              else
                  r[prop] = obj1[prop];
            }
          }
       }
       
    }
    
    return r;
}

https://jsfiddle.net/rv01x6jo/


Here's how to use it:

// To only return the difference
var difference = diff(newValue, oldValue);  

// To exclude certain properties
var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);

Hope this helps someone.

Kildareflare
  • 4,590
  • 5
  • 51
  • 65
a11smiles
  • 1,190
  • 1
  • 9
  • 21
  • Please include the code in your answer as well, not just a fiddle. – xpy May 04 '17 at 09:11
  • It seems like defineProperty would solve this problem with better performance, if i remember correctly it works all the way down to IE9. – Peter Apr 06 '18 at 09:12
  • Thanks..!! Your code works like charm and saved my day. I have json object of 1250 lines and it gives me exact o/p that I want. – Tejas Mehta May 31 '19 at 06:02
  • If you have some parameters not showing up no matter which is your obj1 you can combine them with `Object.assign({}, deepDifference(obj1,obj2), deepDifference(obj1,obj2));` – Daniel Jan 07 '22 at 02:14
4

I composed this for my own use-case (es5 environment), thought this might be useful for someone, so here it is:

function deepCompare(obj1, obj2) {
    var diffObj = Array.isArray(obj2) ? [] : {}
    Object.getOwnPropertyNames(obj2).forEach(function(prop) {
        if (typeof obj2[prop] === 'object') {
            diffObj[prop] = deepCompare(obj1[prop], obj2[prop])
            // if it's an array with only length property => empty array => delete
            // or if it's an object with no own properties => delete
            if (Array.isArray(diffObj[prop]) && Object.getOwnPropertyNames(diffObj[prop]).length === 1 || Object.getOwnPropertyNames(diffObj[prop]).length === 0) {
                delete diffObj[prop]
            }
        } else if(obj1[prop] !== obj2[prop]) {
            diffObj[prop] = obj2[prop]
        }
    });
    return diffObj
}

This might be not really efficient, but will output an object with only different props based on second Obj.

Ordinath
  • 63
  • 5
4

Another lodash based solution which is a bit specific to the case when we want to see the diff for the update made to an object:

const diff = return {
  old: _.pickBy(oldObject, (value, key) => { return !_.isEqual(value, newObject[key]); }),
  new: _.pickBy(newObject, (value, key) => { return !_.isEqual(oldObject[key], value); })
}

Didn't use _.omitBy because of performance implications.

Shivam Sarkhar
  • 154
  • 1
  • 5
3

I've developed the Function named "compareValue()" in Javascript. it returns whether the value is same or not. I've called compareValue() in for loop of one Object. you can get difference of two objects in diffParams.

var diffParams = {};
var obj1 = {"a":"1", "b":"2", "c":[{"key":"3"}]},
    obj2 = {"a":"1", "b":"66", "c":[{"key":"55"}]};

for( var p in obj1 ){
  if ( !compareValue(obj1[p], obj2[p]) ){
    diffParams[p] = obj1[p];
  }
}

function compareValue(val1, val2){
  var isSame = true;
  for ( var p in val1 ) {

    if (typeof(val1[p]) === "object"){
      var objectValue1 = val1[p],
          objectValue2 = val2[p];
      for( var value in objectValue1 ){
        isSame = compareValue(objectValue1[value], objectValue2[value]);
        if( isSame === false ){
          return false;
        }
      }
    }else{
      if(val1 !== val2){
        isSame = false;
      }
    }
  }
  return isSame;
}
console.log(diffParams);
jarangseo
  • 31
  • 4
3

In 2022, A simple algorithm that effectively handles edge cases of comparing two objects, and gives the diff. It does that by flattening the object and comparing the key/value.

The steps:

  • Flatten the objects.
  • Compare Two flattened objects and generate a flattened diff object.
  • Unflatten the diff object.

If you save the flattened object, you can repeat the process and unflatten the diff object only when necessary.

Below is an implementation of the algorithm in JavaScript:

let oldObject = {var1:'value1', var2:{ var1:'value1', var2:'value2'},var3:'value3'};
let newObject = {var2:{ var1:'value11', var3:'value3'},var3:'value3'};

let flatOldObject = flattenObject(oldObject)
/*
{
 'var1':'value1',
 'var2.var1':'value1',
 'var2.var2':'value2',
 'var3':'value3' 
}
*/
let flatNewObject = flattenObject(newObject)
/*
{
 'var2.var1':'value11',
 'var2.var3':'value3',
 'var3':'value3'
}
*/
let flatDiff = diffFlatten(flatOldObject, flatNewObject)
let [updated,removed] = flatDiff
/*
updated = {
 'var2.var1':'value11',
 'var2.var3':'value3'
}
removed = {
'var1':'value1'
}
*/

Note that you can use your own implementation of the steps if you prefer. Here are the implementations that I used:

Implementations

function flattenObject(obj) {
 const object = Object.create(null);
 const path = [];
 const isObject = (value) => Object(value) === value;

 function dig(obj) {
  for (let [key, value] of Object.entries(obj)) {
    path.push(key);
    if (isObject(value)) dig(value);
    else object[path.join('.')] = value;
    path.pop();
  }
 }

 dig(obj);
 return object;
}
function diffFlatten(oldFlat, newFlat) {
    const updated = Object.assign({}, oldFlat);
    const removed = Object.assign({}, newFlat);

    /**delete the unUpdated keys*/
    for (let key in newFlat) {
        if (newFlat[key] === oldFlat[key]) {
             delete updated[key];
             delete removed[key];
        }
    }

    return [updated, removed];

}
function unflatenObject(flattenObject) {
    const unFlatten = Object.create(null);
    for (let [stringKeys, value] of Object.entries(flattenObject)) {
        let chain = stringKeys.split('.')
        let object = unFlatten

        for (let [i, key] of chain.slice(0, -1).entries()) {
            if (!object[key]) {
                let needArray = Number.isInteger(Number(chain[+i + 1]))
                object[key] = needArray ? [] : Object.create(null)
            }
            object = object[key];
        }
        let lastkey = chain.pop();
        object[lastkey] = value;
    }
    return unFlatten;
}
pery mimon
  • 7,713
  • 6
  • 52
  • 57
  • 1
    This pattern makes sense, but a warning for anyone looking at it: this would probably break if there are keys with periods inside of them. An edge case, but worth considering. – Nathan Apr 16 '22 at 23:39
  • Are you mean it flatten before or file name maybe – pery mimon Apr 18 '22 at 08:33
2

Here is a typescript version of @sbgoran code

export class deepDiffMapper {

  static VALUE_CREATED = 'created';
  static VALUE_UPDATED = 'updated';
  static VALUE_DELETED = 'deleted';
  static VALUE_UNCHANGED ='unchanged';

  protected isFunction(obj: object) {
    return {}.toString.apply(obj) === '[object Function]';
  };

  protected isArray(obj: object) {
      return {}.toString.apply(obj) === '[object Array]';
  };

  protected isObject(obj: object) {
      return {}.toString.apply(obj) === '[object Object]';
  };

  protected isDate(obj: object) {
      return {}.toString.apply(obj) === '[object Date]';
  };

  protected isValue(obj: object) {
      return !this.isObject(obj) && !this.isArray(obj);
  };

  protected compareValues (value1: any, value2: any) {
    if (value1 === value2) {
        return deepDiffMapper.VALUE_UNCHANGED;
    }
    if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
        return deepDiffMapper.VALUE_UNCHANGED;
    }
    if ('undefined' == typeof(value1)) {
        return deepDiffMapper.VALUE_CREATED;
    }
    if ('undefined' == typeof(value2)) {
        return deepDiffMapper.VALUE_DELETED;
    }

    return deepDiffMapper.VALUE_UPDATED;
  }

  public map(obj1: object, obj2: object) {
      if (this.isFunction(obj1) || this.isFunction(obj2)) {
          throw 'Invalid argument. Function given, object expected.';
      }
      if (this.isValue(obj1) || this.isValue(obj2)) {
          return {
              type: this.compareValues(obj1, obj2),
              data: (obj1 === undefined) ? obj2 : obj1
          };
      }

      var diff = {};
      for (var key in obj1) {
          if (this.isFunction(obj1[key])) {
              continue;
          }

          var value2 = undefined;
          if ('undefined' != typeof(obj2[key])) {
              value2 = obj2[key];
          }

          diff[key] = this.map(obj1[key], value2);
      }
      for (var key in obj2) {
          if (this.isFunction(obj2[key]) || ('undefined' != typeof(diff[key]))) {
              continue;
          }

          diff[key] = this.map(undefined, obj2[key]);
      }

      return diff;

  }
}
wzr1337
  • 3,609
  • 5
  • 30
  • 53
2

Here is a modified version of something found on gisthub.

isNullBlankOrUndefined = function (o) {
    return (typeof o === "undefined" || o == null || o === "");
}

/**
 * Deep diff between two object, using lodash
 * @param  {Object} object Object compared
 * @param  {Object} base   Object to compare with
 * @param  {Object} ignoreBlanks will not include properties whose value is null, undefined, etc.
 * @return {Object}        Return a new object who represent the diff
 */
objectDifference = function (object, base, ignoreBlanks = false) {
    if (!lodash.isObject(object) || lodash.isDate(object)) return object            // special case dates
    return lodash.transform(object, (result, value, key) => {
        if (!lodash.isEqual(value, base[key])) {
            if (ignoreBlanks && du.isNullBlankOrUndefined(value) && isNullBlankOrUndefined( base[key])) return;
            result[key] = lodash.isObject(value) && lodash.isObject(base[key]) ? objectDifference(value, base[key]) : value;
        }
    });
}
Nico
  • 4,248
  • 1
  • 20
  • 19
1

I just use ramda, for resolve the same problem, i need to know what is changed in new object. So here my design.

const oldState = {id:'170',name:'Ivab',secondName:'Ivanov',weight:45};
const newState = {id:'170',name:'Ivanko',secondName:'Ivanov',age:29};

const keysObj1 = R.keys(newState)

const filterFunc = key => {
  const value = R.eqProps(key,oldState,newState)
  return {[key]:value}
}

const result = R.map(filterFunc, keysObj1)

result is, name of property and it's status.

[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]
Ivan Titkov
  • 359
  • 3
  • 14
1

The more extended and simplified function from sbgoran's answer.
This allow deep scan and find an array's simillarity.

var result = objectDifference({
      a:'i am unchanged',
      b:'i am deleted',
      e: {a: 1,b:false, c: null},
      f: [1,{a: 'same',b:[{a:'same'},{d: 'delete'}]}],
      g: new Date('2017.11.25'),
      h: [1,2,3,4,5]
  },
  {
      a:'i am unchanged',
      c:'i am created',
      e: {a: '1', b: '', d:'created'},
      f: [{a: 'same',b:[{a:'same'},{c: 'create'}]},1],
      g: new Date('2017.11.25'),
      h: [4,5,6,7,8]
  });
console.log(result);

function objectDifference(obj1, obj2){
    if((dataType(obj1) !== 'array' && dataType(obj1) !== 'object') || (dataType(obj2) !== 'array' && dataType(obj2) !== 'object')){
        var type = '';

        if(obj1 === obj2 || (dataType(obj1) === 'date' && dataType(obj2) === 'date' && obj1.getTime() === obj2.getTime()))
            type = 'unchanged';
        else if(dataType(obj1) === 'undefined')
            type = 'created';
        if(dataType(obj2) === 'undefined')
            type = 'deleted';
        else if(type === '') type = 'updated';

        return {
            type: type,
            data:(obj1 === undefined) ? obj2 : obj1
        };
    }
  
    if(dataType(obj1) === 'array' && dataType(obj2) === 'array'){
        var diff = [];
        obj1.sort(); obj2.sort();
        for(var i = 0; i < obj2.length; i++){
            var type = obj1.indexOf(obj2[i]) === -1?'created':'unchanged';
            if(type === 'created' && (dataType(obj2[i]) === 'array' || dataType(obj2[i]) === 'object')){
                diff.push(
                    objectDifference(obj1[i], obj2[i])
                );
                continue;
            }
            diff.push({
                type: type,
                data: obj2[i]
            });
        }

        for(var i = 0; i < obj1.length; i++){
            if(obj2.indexOf(obj1[i]) !== -1 || dataType(obj1[i]) === 'array' || dataType(obj1[i]) === 'object')
                continue;
            diff.push({
                type: 'deleted',
                data: obj1[i]
            });
        }
    } else {
        var diff = {};
        var key = Object.keys(obj1);
        for(var i = 0; i < key.length; i++){
            var value2 = undefined;
            if(dataType(obj2[key[i]]) !== 'undefined')
                value2 = obj2[key[i]];

            diff[key[i]] = objectDifference(obj1[key[i]], value2);
        }

        var key = Object.keys(obj2);
        for(var i = 0; i < key.length; i++){
            if(dataType(diff[key[i]]) !== 'undefined')
                continue;

            diff[key[i]] = objectDifference(undefined, obj2[key[i]]);
        }
    }

    return diff;
}

function dataType(data){
    if(data === undefined || data === null) return 'undefined';
    if(data.constructor === String) return 'string';
    if(data.constructor === Array) return 'array';
    if(data.constructor === Object) return 'object';
    if(data.constructor === Number) return 'number';
    if(data.constructor === Boolean) return 'boolean';
    if(data.constructor === Function) return 'function';
    if(data.constructor === Date) return 'date';
    if(data.constructor === RegExp) return 'regex';
    return 'unknown';
}
StefansArya
  • 2,802
  • 3
  • 24
  • 25
1

I have used this piece of code for doing the task that you describe:

function mergeRecursive(obj1, obj2) {
    for (var p in obj2) {
        try {
            if(obj2[p].constructor == Object) {
                obj1[p] = mergeRecursive(obj1[p], obj2[p]);
            }
            // Property in destination object set; update its value.
            else if (Ext.isArray(obj2[p])) {
                // obj1[p] = [];
                if (obj2[p].length < 1) {
                    obj1[p] = obj2[p];
                }
                else {
                    obj1[p] = mergeRecursive(obj1[p], obj2[p]);
                }

            }else{
                obj1[p] = obj2[p];
            }
        } catch (e) {
            // Property in destination object not set; create it and set its value.
            obj1[p] = obj2[p];
        }
    }
    return obj1;
}

this will get you a new object that will merge all the changes between the old object and the new object from your form

AMember
  • 3,037
  • 2
  • 33
  • 64
  • 1
    I am using the Ext framework here but you can replace it and use what ever other framework you'd like... – AMember Dec 21 '11 at 15:37
  • Merging objects are trivial and can be done as easy as `$.extend(true,obj1,obj2)` using jQuery. This is not at all what I need. I need the difference between the two objects not the combination of them. – Martin Jespersen Dec 21 '11 at 18:03
  • its great that Ext is used in here – peroxide Mar 11 '15 at 11:29
0

I already wrote a function for one of my projects that will comparing an object as a user options with its internal clone. It also can validate and even replace by default values if the user entered bad type of data or removed, in pure javascript.

In IE8 100% works. Tested successfully.

//  ObjectKey: ["DataType, DefaultValue"]
reference = { 
    a : ["string", 'Defaul value for "a"'],
    b : ["number", 300],
    c : ["boolean", true],
    d : {
        da : ["boolean", true],
        db : ["string", 'Defaul value for "db"'],
        dc : {
            dca : ["number", 200],
            dcb : ["string", 'Default value for "dcb"'],
            dcc : ["number", 500],
            dcd : ["boolean", true]
      },
      dce : ["string", 'Default value for "dce"'],
    },
    e : ["number", 200],
    f : ["boolean", 0],
    g : ["", 'This is an internal extra parameter']
};

userOptions = { 
    a : 999, //Only string allowed
  //b : ["number", 400], //User missed this parameter
    c: "Hi", //Only lower case or case insitive in quotes true/false allowed.
    d : {
        da : false,
        db : "HelloWorld",
        dc : {
            dca : 10,
            dcb : "My String", //Space is not allowed for ID attr
            dcc: "3thString", //Should not start with numbers
            dcd : false
      },
      dce: "ANOTHER STRING",
    },
    e: 40,
    f: true,
};


function compare(ref, obj) {

    var validation = {
        number: function (defaultValue, userValue) {
          if(/^[0-9]+$/.test(userValue))
            return userValue;
          else return defaultValue;
        },
        string: function (defaultValue, userValue) {
          if(/^[a-z][a-z0-9-_.:]{1,51}[^-_.:]$/i.test(userValue)) //This Regex is validating HTML tag "ID" attributes
            return userValue;
          else return defaultValue;
        },
        boolean: function (defaultValue, userValue) {
          if (typeof userValue === 'boolean')
            return userValue;
          else return defaultValue;
        }
    };

    for (var key in ref)
        if (obj[key] && obj[key].constructor && obj[key].constructor === Object)
          ref[key] = compare(ref[key], obj[key]);
        else if(obj.hasOwnProperty(key))
          ref[key] = validation[ref[key][0]](ref[key][1], obj[key]); //or without validation on user enties => ref[key] = obj[key]
        else ref[key] = ref[key][1];
    return ref;
}

//console.log(
    alert(JSON.stringify( compare(reference, userOptions),null,2 ))
//);

/* result

{
  "a": "Defaul value for \"a\"",
  "b": 300,
  "c": true,
  "d": {
    "da": false,
    "db": "Defaul value for \"db\"",
    "dc": {
      "dca": 10,
      "dcb": "Default value for \"dcb\"",
      "dcc": 500,
      "dcd": false
    },
    "dce": "Default value for \"dce\""
  },
  "e": 40,
  "f": true,
  "g": "This is an internal extra parameter"
}

*/
0

I stumbled here trying to look for a way to get the difference between two objects. This is my solution using Lodash:

// Get updated values (including new values)
var updatedValuesIncl = _.omitBy(curr, (value, key) => _.isEqual(last[key], value));

// Get updated values (excluding new values)
var updatedValuesExcl = _.omitBy(curr, (value, key) => (!_.has(last, key) || _.isEqual(last[key], value)));

// Get old values (by using updated values)
var oldValues = Object.keys(updatedValuesIncl).reduce((acc, key) => { acc[key] = last[key]; return acc; }, {});

// Get newly added values
var newCreatedValues = _.omitBy(curr, (value, key) => _.has(last, key));

// Get removed values
var deletedValues = _.omitBy(last, (value, key) => _.has(curr, key));

// Then you can group them however you want with the result

Code snippet below:

var last = {
"authed": true,
"inForeground": true,
"goodConnection": false,
"inExecutionMode": false,
"online": true,
"array": [1, 2, 3],
"deep": {
 "nested": "value",
},
"removed": "value",
};

var curr = {
"authed": true,
"inForeground": true,
"deep": {
 "nested": "changed",
},
"array": [1, 2, 4],
"goodConnection": true,
"inExecutionMode": false,
"online": false,
"new": "value"
};

// Get updated values (including new values)
var updatedValuesIncl = _.omitBy(curr, (value, key) => _.isEqual(last[key], value));
// Get updated values (excluding new values)
var updatedValuesExcl = _.omitBy(curr, (value, key) => (!_.has(last, key) || _.isEqual(last[key], value)));
// Get old values (by using updated values)
var oldValues = Object.keys(updatedValuesIncl).reduce((acc, key) => { acc[key] = last[key]; return acc; }, {});
// Get newly added values
var newCreatedValues = _.omitBy(curr, (value, key) => _.has(last, key));
// Get removed values
var deletedValues = _.omitBy(last, (value, key) => _.has(curr, key));

console.log('oldValues', JSON.stringify(oldValues));
console.log('updatedValuesIncl', JSON.stringify(updatedValuesIncl));
console.log('updatedValuesExcl', JSON.stringify(updatedValuesExcl));
console.log('newCreatedValues', JSON.stringify(newCreatedValues));
console.log('deletedValues', JSON.stringify(deletedValues));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
Jee Mok
  • 6,157
  • 8
  • 47
  • 80
0

I took the answer above by @sbgoran and modified it for my case same as the question needed, to treat arrays as sets (i.e. order is not important for diff)

const deepDiffMapper = function () {
return {
  VALUE_CREATED: "created",
  VALUE_UPDATED: "updated",
  VALUE_DELETED: "deleted",
  VALUE_UNCHANGED: "unchanged",
  map: function(obj1: any, obj2: any) {
    if (this.isFunction(obj1) || this.isFunction(obj2)) {
      throw "Invalid argument. Function given, object expected.";
    }
    if (this.isValue(obj1) || this.isValue(obj2)) {
      return {
        type: this.compareValues(obj1, obj2),
        data: obj2 === undefined ? obj1 : obj2
      };
    }

    if (this.isArray(obj1) || this.isArray(obj2)) {
      return {
        type: this.compareArrays(obj1, obj2),
        data: this.getArrayDiffData(obj1, obj2)
      };
    }

    const diff: any = {};
    for (const key in obj1) {

      if (this.isFunction(obj1[key])) {
        continue;
      }

      let value2 = undefined;
      if (obj2[key] !== undefined) {
        value2 = obj2[key];
      }

      diff[key] = this.map(obj1[key], value2);
    }
    for (const key in obj2) {
      if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
        continue;
      }

      diff[key] = this.map(undefined, obj2[key]);
    }

    return diff;

  },

  getArrayDiffData: function(arr1: Array<any>, arr2: Array<any>) {
    const set1 = new Set(arr1);
    const set2 = new Set(arr2);

    if (arr1 === undefined || arr2 === undefined) {
       return arr1 === undefined ? arr1 : arr2;
    }
    const deleted = [...arr1].filter(x => !set2.has(x));

    const added = [...arr2].filter(x => !set1.has(x));

    return {
      added, deleted
    };

  },

  compareArrays: function(arr1: Array<any>, arr2: Array<any>) {
    const set1 = new Set(arr1);
    const set2 = new Set(arr2);
    if (_.isEqual(_.sortBy(arr1), _.sortBy(arr2))) {
      return this.VALUE_UNCHANGED;
    }
    if (arr1 === undefined) {
      return this.VALUE_CREATED;
    }
    if (arr2 === undefined) {
      return this.VALUE_DELETED;
    }
    return this.VALUE_UPDATED;
  },
  compareValues: function (value1: any, value2: any) {
    if (value1 === value2) {
      return this.VALUE_UNCHANGED;
    }
    if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
      return this.VALUE_UNCHANGED;
    }
    if (value1 === undefined) {
      return this.VALUE_CREATED;
    }
    if (value2 === undefined) {
      return this.VALUE_DELETED;
    }
    return this.VALUE_UPDATED;
  },
  isFunction: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Function]";
  },
  isArray: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Array]";
  },
  isDate: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Date]";
  },
  isObject: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Object]";
  },
  isValue: function (x: any) {
    return !this.isObject(x) && !this.isArray(x);
  }
 };
}();
Bashar Ali Labadi
  • 1,014
  • 1
  • 11
  • 19
0

This returns new object only with changed properties. (omit and isEmpty are functions from lodash)

export const getObjectDifference = <T extends {}>(originalObject: T, newObject: T) => {
const sameProperties: string[] = [];

Object.entries(originalObject).forEach(original => {
    Object.entries(newObject).forEach(newObj => {
        if (original[0] === newObj[0]) {
            if (original[1] === newObj[1])
                sameProperties.push(newObj[0]);
        }
    });
});

const objectDifference: T = omit(newObject, sameProperties) as T;
if (isEmpty(objectDifference))
    return null;
else
    return objectDifference; }
0

this will treat [1,2,3] and [3,2,1] as equal (deep object)
since I needed to visualize the difference between:

[
  {
    "a":1,
    "b":1
  },
  {
    "a":1,
    "b":1
  }
]

and

[
  {
    "a":1,
    "b":1
  },
  {
    "a":"OH NO",
    "b":"an insertion"
  },
  {
    "a":1,
    "b":1
  }
]

so I wanted to see them collide, here's what's left:

[]
and
[
  {
    "a":"OH NO",
    "b":"an insertion"
  }
]

imo this is the best way to represent it. {add:{...},upd:{...},del:{...}} is hard to read


I provide 2 functions : ObjectCollide(obj1,obj2) and ArrayCollide(arr1,arr2)

console.log(ArrayCollide([1,2,3],[3,2,1]))
// false
//everything collided -> false
console.log(ArrayCollide([1],[2,1]))
// [ [], [ 2 ] ]
//1 and 1 collided, even if they are on different indices

//array of objects
const arr1 = 
[
  {
    "a":1,
    "b":1
  },
  {
    "a":1,
    "b":1
  }
]
const arr2 = 
[
  {
    "a":1,
    "b":1
  },
  {
    "a":"OH NO",
    "b":"an insertion"
  },
  {
    "a":1,
    "b":1
  }
]
const newArrays = ArrayCollide(arr1, arr2)
console.log(newArrays[0])
console.log(newArrays[1])
console.log('\n')
// []
// [ { a: 'OH NO', b: 'an insertion' } ]
// everything collided until this is left

//ObjectCollide
const obj1 = { a: '111', c: { q: 'no', a: '333' } }
const obj2 = { a: '111', p: 'ok', c: { a: '333' } }
ObjectCollide(obj1, obj2) //in place
console.log(obj1)
console.log(obj2)
console.log('\n')
// { c: { q: 'no' } }
// { p: 'ok', c: {} }
// obj["a"] collided and obj["c"]["a"] collided

//testing empty array
const a1 = { a: [] }
const a2 = { a: [], b: '2' }
ObjectCollide(a1, a2) //in place
console.log(a1)
console.log(a2)
console.log('\n')
// {}
// { b: '2' }
// obj["a"] collided

//DIFFERENT TYPES
const b1 = {a:true}
const b2 = {a:[1,2]}
ObjectCollide(b1,b2) //in place
console.log(b1)
console.log(b2)
// { a: true }
// { a: [ 1, 2 ] }

function ObjectCollide(obj1, obj2) {
  //in place, returns true if same

  // delete same
  const keys = Object.keys(obj1)
  const len = keys.length
  let howManyDeleted = 0
  for (let i = 0; i < len; i++) {
    const key = keys[i]

    const type1 = Array.isArray(obj1[key]) === true ? 'array' : typeof obj1[key]
    const type2 = Array.isArray(obj2[key]) === true ? 'array' : typeof obj2[key]
    if (type1!==type2) {
      continue
    }
    switch (type1) {
      case 'object':
        if (ObjectCollide(obj1[key], obj2[key])) {
          delete obj1[key]
          delete obj2[key]
          howManyDeleted++
        }
        continue
      case 'array':
        const newArrays = ArrayCollide(obj1[key], obj2[key])
        if (newArrays) {
          obj1[key] = newArrays[0]
          obj2[key] = newArrays[1]
        } else {
          delete obj1[key]
          delete obj2[key]
          howManyDeleted++
        }
        continue
      default:
        //string, number, I hope it covers everything else
        if (obj1[key] === obj2[key]) {
          delete obj1[key]
          delete obj2[key]
          howManyDeleted++
        }
    }
  }


  if (howManyDeleted === len && Object.keys(obj2).length === 0) {
    // return 'delete the stuff'
    // same. we've deleted everything!
    return true
  }

}
function ArrayCollide(arr1, arr2) {
  // returns [newArr1, newArr2] or false if same arrays (ignore order)
  const stringifyObj = {}

  const newArr1 = []
  const newArr2 = []
  for (let i = 0, len = arr1.length; i < len; i++) {
    const value = arr1[i]
    const stringified = JSON.stringify(value)
    stringifyObj[stringified]
    // arr = [count, ...]
    const arr = stringifyObj[stringified] || (stringifyObj[stringified] = [0])
    arr[0]++
    arr.push(value)
  }
  //in 2 but not in 1
  for (let i = 0, len = arr2.length; i < len; i++) {
    const value = arr2[i]
    const stringified = JSON.stringify(value)
    const arr = stringifyObj[stringified]
    if (arr === undefined) {
      newArr2.push(value)
    } else {
      if (arr[0] === 0) {
        newArr2.push(value)
      } else {
        arr[0]--
      }
    }
  }
  //in 1 but not in 2
  stringifyKeys = Object.keys(stringifyObj)
  for (let i = 0, len = stringifyKeys.length; i < len; i++) {
    const arr = stringifyObj[stringifyKeys[i]]

    for (let i = 1, len = arr[0] + 1; i < len; i++) {
      newArr1.push(arr[i])
    }
  }
  if (newArr1.length || newArr2.length) {
    return [newArr1, newArr2]
  } else {
    return false
  }

}

what I was trying to solve:

JSON file keeps reordering, I want to revert the JSON if it's equivalent: like {a:1,b:2} and {b:2,a:1}
but because I don't trust my code (I made a mistake once), I want to see the diff and check it myself, I can Ctrl+F into the original file using this diff.

Mr. Doge
  • 796
  • 5
  • 11
0

Below method will create a new object with only changed fields

const findDiff = (obj1, obj2) => {
  const isNativeType1 = typeof obj1 !== "object";
  const isNativeType2 = typeof obj2 !== "object";
  if (isNativeType1 && isNativeType2) {
    return obj1 === obj2 ? null : obj2;
  }
  if (isNativeType1 && !isNativeType2) {
    return obj2;
  }
  if (!isNativeType1 && isNativeType2) {
    return obj2;
  }
  const isArray1 = Array.isArray(obj1);
  const isArray2 = Array.isArray(obj2);
  if (isArray1 && isArray2) {
    const firstLenght = obj1.length;
    const secondLenght = obj2.length;
    const hasSameLength = firstLenght === secondLenght;
    if (!hasSameLength) return obj2;
    let hasChange = false;
    for (let index = 0; index < obj1.length; index += 1) {
      const element1 = obj1[index];
      const element2 = obj2[index];
      const changed = findDiff(element1, element2);
      if (changed) {
        hasChange = true;
      }
    }
    return hasChange ? obj2 : null;
  }
  if (isArray1 || isArray2) return obj2;
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  const hasSameKeys = keys1.length === keys2.length;
  if (!hasSameKeys) {
    const retObj = { ...obj2 };
    for (let index = 0; index < keys1.length; index += 1) {
      const key = keys1[index];
      if (!keys2.includes(key)) {
        retObj[key] = null;
        // eslint-disable-next-line no-continue
        continue;
      }
      delete retObj[key];
    }
    return retObj;
  }
  let hasChange = false;
  const retObj = {};
  for (let index = 0; index < keys1.length; index += 1) {
    const key = keys1[index];
    const element1 = obj1[key];
    const element2 = obj2[key];
    const changed = findDiff(element1, element2);
    if (changed) {
      hasChange = true;
    }
    if (changed) {
      retObj[key] = changed;
    }
  }
  return hasChange ? retObj : null;
};

console.log(
  JSON.stringify(findDiff(
    {
      a: 1,
      b: 2,
      c: {
        a: ['1', 'b', { a: 'b', c: false }, true],
      },
    },
    {
      a: 1,
      b: 2,
      c: {
        a: ['1','b', { a: 'b', c: true }, true],
      },
    }
  ), null, 2)
);
Siyahul Haq
  • 329
  • 4
  • 6
0
var base = [
{"value": "01", "label": "Pendências"},
{"value": "02", "label": "Ambulatório"},
{"value": "03", "label": "Urgência"},
{"value": "04", "label": "Clínica Médica"},
{"value": "05", "label": "Revisão"},
{"value": "06", "label": "Imagens"},
];

var used = [
{"value": "01", "label": "Pendências"},
{"value": "02", "label": "Ambulatório"},
{"value": "03", "label": "Urgência"},
{"value": "04", "label": "Clínica Médica"},
];

function diff(obj1,obj2) {
        var temp = JSON.stringify(obj2.map((x)=> x.value));
    return obj1.filter((y)=> temp.indexOf(y.value)<0 && y);
}


var result = diff(base, used); 
console.clear();
console.log('RESULTADO');
console.log(result);

codeped

Thiago Pires
  • 148
  • 1
  • 4
0
function Difference (ob1,ob2){   
 let ob3={}  
 let status=false  
  for ( var a1 in ob1 ) {  
    for (var a2 in ob2){  
      if (a1===a2 && ob1[a1]===ob2[a2]){     
        status=true;   
        break;   
      };
    };    
    if (status===false){    
      if (ob1[a2]===undefined){    
        ob3[a1]="ob1:"+ob1[a1]+", ob2:"+ob2[a1];    
      };    
      if ( ob2[a1]===undefined){    
        ob3[a2]="ob1:"+ob1[a2]+", ob2:"+ob2[a2];    
      }else {    
        ob3[a1]="ob1:"+ob1[a1] +", ob2:"+ob2[a1];    
      };     
    }else {   
      status=false;
    };   
  };     
  console.log(ob3);    
};
  • Welcome to SO. Please refer to ["how do I write a good answer"](https://stackoverflow.com/help/how-to-answer) and explain your approach a little in textual form: How does it work? What makes it better/different from the other answers? – ahuemmer Nov 30 '22 at 13:21
0

For a simple object diff I like the very straightforward:

function simpleObjectDiff(obj1, obj2) {
  return Object.fromEntries(
    Object.entries(obj1)
     .filter(([key, value]) => value !== obj2[key])
  )
}
KenCorbettJr
  • 196
  • 1
  • 3
0

I just made a small modification to the most upvoted answer for my case. Hope someone else finds it useful. Typescript support and ability to just find specific changes for a given diff. Output shown below for same case that was provided but only looking for deleted changes.

export enum DiffMapperType {
  VALUE_CREATED = 'created',
  VALUE_UPDATED = 'updated',
  VALUE_DELETED = 'deleted',
  VALUE_UNCHANGED = 'unchanged',
}
export const deepDiffMapper = function () {
  return {
    compareData: function(obj1: any, obj2: any) {
      if (this.isFunction(obj1) || this.isFunction(obj2)) {
        throw 'Invalid argument. Function given, object expected.';
      }
      if (this.isValue(obj1) || this.isValue(obj2)) {
        return {
          type: this.compareValues(obj1, obj2),
          data: obj1 === undefined ? obj2 : obj1
        };
      }

      const diff: any = {};
      for (const key in obj1) {
        if (this.isFunction(obj1[key])) {
          continue;
        }

        let value2 = undefined;
        if (obj2[key] !== undefined) {
          value2 = obj2[key];
        }

        diff[key] = this.compareData(obj1[key], value2);
      }
      for (const key in obj2) {
        if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
          continue;
        }

        diff[key] = this.compareData(undefined, obj2[key]);
      }

      return diff;
    },
    validateDiff: function(compared: any, diffMapperTypes: DiffMapperType[]) {
      if (this.isResult(compared)) {
        return this.includeResult(compared, diffMapperTypes);
      }

      const diff: any = {};
      for (const key in compared) {
        if (this.isFunction(compared[key])) {
          continue;
        }

        const validatedDiff = this.validateDiff(compared[key], diffMapperTypes);
        if (validatedDiff !== undefined) {
          diff[key] = validatedDiff;
        }
      }
      if (diffMapperTypes.length !== Object.keys(DiffMapperType).length && !Object.values(diff).filter(d => d).length) {
        return undefined;
      }
      return diff;
    },
    map: function(obj1: any, obj2: any, diffMapperTypes: DiffMapperType[] = [DiffMapperType.VALUE_CREATED, DiffMapperType.VALUE_DELETED, DiffMapperType.VALUE_UPDATED, DiffMapperType.VALUE_UNCHANGED]) {
      const compared = this.compareData(obj1, obj2);
      return this.validateDiff(compared, diffMapperTypes);
    },
    unchanged: function(value1: any, value2: any) {
      if (value1 === value2) {
        return true;
      }
      return this.isDate(value1) && this.isDate(value2) && (value1 as Date).getTime() === (value2 as Date).getTime();
    },
    compareValues: function (value1: object, value2: object) {
      if (this.unchanged(value1,  value2)) {
        return DiffMapperType.VALUE_UNCHANGED;
      }
      if (value1 === undefined) {
        return DiffMapperType.VALUE_CREATED;
      }
      if (value2 === undefined) {
        return DiffMapperType.VALUE_DELETED;
      }
      return DiffMapperType.VALUE_UPDATED;
    },
    includeResult: function (result: { type: string, data: any }, diffMapperTypes: DiffMapperType[]) {
      return Object.values(diffMapperTypes).map(d => d.toString()).includes(result.type) ? result : undefined;
    },
    isFunction: function (x: any) {
      return Object.prototype.toString.call(x) === '[object Function]';
    },
    isArray: function (x: any) {
      return Object.prototype.toString.call(x) === '[object Array]';
    },
    isDate: function (x: any) {
      return Object.prototype.toString.call(x) === '[object Date]';
    },
    isObject: function (x: any) {
      return Object.prototype.toString.call(x) === '[object Object]';
    },
    isValue: function (x: any) {
      return !this.isObject(x) && !this.isArray(x);
    },
    isResult: function(x: any) {
      return this.isObject(x) && x.type && x.data;
    }
  }
}();


var result = deepDiffMapper.map({
  a: 'i am unchanged',
  b: 'i am deleted',
  e: {
    a: 1,
    b: false,
    c: null
  },
  f: [1, {
    a: 'same',
    b: [{
      a: 'same'
    }, {
      d: 'delete'
    }]
  }],
  g: new Date('2017.11.25')
}, {
  a: 'i am unchanged',
  c: 'i am created',
  e: {
    a: '1',
    b: '',
    d: 'created'
  },
  f: [{
    a: 'same',
    b: [{
      a: 'same'
    }, {
      c: 'create'
    }]
  }, 1],
  g: new Date('2017.11.25')
}, [DiffMapperType.VALUE_DELETED]);

output looks like:

{
  "b": {
    "type": "deleted",
    "data": "i am deleted"
  },
  "e": {
    "c": {
      "type": "deleted",
      "data": null
    }
  }
}
alleles
  • 1
  • 2
  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 11 '23 at 11:11