15

I have a rich AJAX-based web application that uses JQuery + Knockout. I have a JQuery plugin that wraps my Knockout view models to expose utility methods like .reset(), .isDirty(), and so on.

I have a method called .setBaseline() that essentially takes a snapshot of the data model once it has been populated (via the mapping plugin). Then I can use this snapshot to quickly determine if the model has changed.

What I'm looking for is some kind of general purpose function that can return an object that represents the differences between two 2 JavaScript objects where one of the objects is considered to be the master.

For example, assume that this is my snapshot:

var snapShot = {
  name: "Joe",
  address: "123 Main Street",
  age: 30,
  favoriteColorPriority: {
     yellow: 1,
     pink: 2,
     blue: 3
  }
};

Then assume that the live data looks like this:

var liveData = {
    name: "Joseph",
    address: "123 Main Street",
    age: 30,
    favoriteColorPriority: {
        yellow: 1,
        pink: 3,
        blue: 2
    }
};

I want a .getChanges(snapShot, liveData) utility function that returns the following:

var differences = {
    name: "Joseph",
    favoriteColorPriority: {
        pink: 3,
        blue: 2
    }
};

I was hoping that the _.underscore library might have something like this, but I couldn't find anything that seemed to work like this.

Armchair Bronco
  • 2,367
  • 4
  • 31
  • 44
  • I saw this same question asked this morning. – epascarello Jun 13 '12 at 19:24
  • Brilliant minds think alike! :-) Do you have a link? – Armchair Bronco Jun 13 '12 at 19:25
  • 1
    @epascarello http://stackoverflow.com/questions/11016857/remove-default-values-from-an-object perhaps? – apsillers Jun 13 '12 at 19:30
  • Thanks. I'm reviewing the suggestions in that Question to see if anything suits my use case. So far, the author of that Question hasn't accepted an answer so I'd like to let this question run a bit longer. – Armchair Bronco Jun 13 '12 at 19:36
  • http://stackoverflow.com/questions/264430/how-can-i-get-a-list-of-the-differences-between-two-javascript-object-graphs – epascarello Jun 13 '12 at 19:37
  • The solution proposed for 264430 above looks like it could be a maintenance nightmare, so that solution won't work for me. Too many multiple if statements and special casing. Looks like a potential bug farm to me at first glance. – Armchair Bronco Jun 13 '12 at 19:45

7 Answers7

17

I don't think there is such a function in underscore, but it's easy to implement yourself:

function getChanges(prev, now) {
    var changes = {};
    for (var prop in now) {
        if (!prev || prev[prop] !== now[prop]) {
            if (typeof now[prop] == "object") {
                var c = getChanges(prev[prop], now[prop]);
                if (! _.isEmpty(c) ) // underscore
                    changes[prop] = c;
            } else {
                changes[prop] = now[prop];
            }
        }
    }
    return changes;
}

or

function getChanges(prev, now) {
    var changes = {}, prop, pc;
    for (prop in now) {
        if (!prev || prev[prop] !== now[prop]) {
            if (typeof now[prop] == "object") {
                if(c = getChanges(prev[prop], now[prop]))
                    changes[prop] = c;
            } else {
                changes[prop] = now[prop];
            }
        }
    }
    for (prop in changes)
        return changes;
    return false; // false when unchanged
}

This will not work with Arrays (or any other non-plain-Objects) or differently structured objects (removals, primitive to object type changes).

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    Removed my answer in favor of this. – Radu Jun 13 '12 at 20:06
  • These these now using *real* data. One thing I forgot to mention is that if the snapShot contains data missing from the current (live) data, then I don't want to see those values in the changeList. For example, my shapShot data model includes an empty object called 'businessUnitProperties: {}' This might be a key/value pair. If the live data doesn't have this member, then changes shouldn't return it either. – Armchair Bronco Jun 13 '12 at 20:09
  • One possible issue with this could stem from the use of `typeof` which is notoriously unreliable. As long as you're aware of the caveats you should be fine though. – Radu Jun 13 '12 at 20:12
  • For typeof() issues, I'll use the _.underscore wrapper _.isObject(). However, so far so good. – Armchair Bronco Jun 13 '12 at 20:16
  • Bergi, of your two implementations above, which one is your preference? I've testing the first one [FIFO - :-)] and it works great! Before accepting the answer, I'd like you to weigh in on the subtle differences between the 2 flavors. – Armchair Bronco Jun 13 '12 at 20:18
  • No point in waiting. The first version works like a charm and is clean and elegant. I changed: if (typeof now[prop] == "object") to: if (_.isObject(now[prop]) and added curly braces for the 3rd level nested "if" (to make JSLint happy). Thanks again for a "Less is More" solution! – Armchair Bronco Jun 13 '12 at 20:24
  • @ArmchairBronco: @ your first comment: Key-value pairs missing in the current data won't be recognized, only those missing in the older snapshot (the new ones) will be included. @ the two implementations: I'd prefer the second one because it does not need isEmpty(). But that depends on whether you like getting (boolean) false for no changes - your choice. – Bergi Jun 13 '12 at 21:14
  • Thanks for that, Bergi. Just curious: can your solution be extended to also support arrays that are embedded in the JSON? – Armchair Bronco Jun 13 '12 at 22:29
  • 1
    Yes, of course. Could you add one to your example in the question, so that I can see how you would want changes to them? The simple method would be treating them like primitive values, just add `&& !Array.isArray(now[prop])` after the `typeof now[prop] == "object"`... – Bergi Jun 13 '12 at 23:40
  • Thanks. Tomorrow, I'll track down an example of some live data from our RESTful API that includes embedded arrays in the JSON returned by our webservices. I'll also make sure I account for any other edge cases. Most of our API's return JS primitives like strings, bools, ints and simple maps. But IIRC a few also return arrays. Anyway, the same you gave me earlier has already been checked in and is getting called for all my update operations. – Armchair Bronco Jun 14 '12 at 01:32
  • I added my own code for handling arrays below (it actually includes the complete implementation with some checks that were not part of the initial, simplified question). – Armchair Bronco Jan 22 '13 at 22:12
  • Works like charm... Superb... @Bergi – Shubh Mar 13 '13 at 22:23
3

Posting my own answer so folks can see the final implementation that also works with arrays. In the code below, "um" is my namespace, and I'm also using the _.isArray() and _.isObject methods from Underscore.js.

The code that looks for "_KO" is used to skip past Knockout.js members that are present in the object.

// This function compares 'now' to 'prev' and returns a new JavaScript object that contains only
// differences between 'now' and 'prev'. If 'prev' contains members that are missing from 'now',
// those members are *not* returned. 'now' is treated as the master list.
um.utils.getChanges = function (prev, now) {
    var changes = {};
    var prop = {};
    var c = {};
    //-----

    for (prop in now) { //ignore jslint
        if (prop.indexOf("_KO") > -1) {
            continue; //ignore jslint
        }

        if (!prev || prev[prop] !== now[prop]) {
            if (_.isArray(now[prop])) {
                changes[prop] = now[prop];
            }
            else if (_.isObject(now[prop])) {
                // Recursion alert
                c = um.utils.getChanges(prev[prop], now[prop]);
                if (!_.isEmpty(c)) {
                    changes[prop] = c;
                }
            } else {
                changes[prop] = now[prop];
            }
        }
    }

    return changes;
};
Armchair Bronco
  • 2,367
  • 4
  • 31
  • 44
1

I am using JsonDiffPatch in my projects to find the delta, transfer it over the net, and then patch the object at the other end to get the exact copy. It is very easy to use and works really well.

And it works with arrays too!

srgstm
  • 3,649
  • 2
  • 24
  • 26
  • I'll take a look at this library. I've already custom fitted my own solution using the code above as a starting point, so it's probably too late in the game to switch to something new when what I have now isn't broken. But it's great to see some standalone libraries for doing these kinds of comparisons in a generic way. – Armchair Bronco Feb 07 '14 at 19:58
0

Solution with the use of jQuery.isEmptyObject()

This reworks the getChanges(prev, now) solution above. It should work also on objects of different type and also when prev[prop] is undefined (resulting to now[prop])

function getChanges(prev, now)
{
    // sanity checks, now and prev must be an objects
    if (typeof now !== "object")
        now = {};
    if (typeof prev !== "object")
        return now;

    var changes = {};
    for (var prop in now) {
         // if prop is new in now, add it to changes
        if (!prev || !prev.hasOwnProperty(prop) ) {
            changes[prop] = now[prop];
            continue;
        }
        // if prop has another type or value (different object or literal)
        if (prev[prop] !== now[prop]) {
            if (typeof now[prop] === "object") {
                // prop is an object, do recursion
                var c = getChanges(prev[prop], now[prop]);
                if (!$.isEmptyObject(c))
                    changes[prop] = c;
            } else {
                // now[prop] has different but literal value
                changes[prop] = now[prop];
            }
        }
    }

    // returns empty object on none change
    return changes;
}
jozop
  • 23
  • 3
  • This seems like a viable solution, except produces a "Uncaught RangeError: Maximum call stack size exceeded" on most large nested objects I have attempted it on. If you adjust the code to not use jQuery, you may be able to get a performance boost out of it. In theory of course. This works great on smaller nested objects though. – NinjaKC Dec 18 '15 at 03:44
0

No need to use jquery for this. I recently wrote a module to do this: 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}]}
]
*/

In the readme for odiff, I listed out other modules that I determined didn't fit my needs if you want to check those out too.

B T
  • 57,525
  • 34
  • 189
  • 207
0

Based on the solution above. This version corrects a bug, some changes are not detected with the original solution.

getChanges = function (prev, now) {
        var changes = {}, prop, pc;
        for (prop in now) {
            if (!prev || prev[prop] !== now[prop]) {
                if (typeof now[prop] == "object") {
                    if (c = getChanges(prev[prop], now[prop]))
                        changes[prop] = c;
                } else {
                    changes[prop] = now[prop];
                }
            }
        }

        for (prop in prev) {
            if (!now || now[prop] !== prev[prop]) {
                if (typeof prev[prop] == "object") {
                    if (c = getChanges(now[prop], prev[prop]))
                        changes[prop] = c;
                } else {
                    changes[prop] = prev[prop];
                }
            }
        }

        return changes;
    };
Carlos
  • 159
  • 2
  • 3
0

Be aware that typeof now[prop] == "object" is even true if now[prop] is a Date.

Instead of: typeof now[prop] == "object"

I used: Object.prototype.toString.call( now[prop] === "[object Object]")

Matthias
  • 3
  • 2