jQuery function to compute the difference between two JavaScript objects

10,721

Solution 1

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

Solution 2

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

Solution 3

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!

Share:
10,721
Armchair Bronco
Author by

Armchair Bronco

CEO & Co-Founder of http://ipredikt.com, a predictions-based social media entertainment built around user-generated predictions. "Vote early, vote often" at http://ipredikt.com. See "Tomorrow's News, Today"

Updated on July 09, 2022

Comments

  • Armchair Bronco
    Armchair Bronco almost 2 years

    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.

  • Radu
    Radu almost 12 years
    Removed my answer in favor of this.
  • Armchair Bronco
    Armchair Bronco almost 12 years
    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.
  • Radu
    Radu almost 12 years
    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.
  • Armchair Bronco
    Armchair Bronco almost 12 years
    For typeof() issues, I'll use the _.underscore wrapper _.isObject(). However, so far so good.
  • Armchair Bronco
    Armchair Bronco almost 12 years
    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
    Armchair Bronco almost 12 years
    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!
  • Bergi
    Bergi almost 12 years
    @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.
  • Armchair Bronco
    Armchair Bronco almost 12 years
    Thanks for that, Bergi. Just curious: can your solution be extended to also support arrays that are embedded in the JSON?
  • Bergi
    Bergi almost 12 years
    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"...
  • Armchair Bronco
    Armchair Bronco almost 12 years
    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
    Armchair Bronco over 11 years
    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).
  • Shubh
    Shubh about 11 years
    Works like charm... Superb... @Bergi
  • Armchair Bronco
    Armchair Bronco over 10 years
    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.
  • NinjaKC
    NinjaKC over 8 years
    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.