Функция jQuery для вычисления разницы между двумя объектами JavaScript

У меня есть богатое веб-приложение на основе AJAX, которое использует JQuery + нокаут. У меня есть плагин JQuery, который обертывает мои модели представления нокаутом, чтобы выставлять полезные методы, такие как .reset(),.isDirty() и т.д.

У меня есть метод под названием .setBaseline(), который по существу принимает моментальный снимок модели данных после ее заполнения (через плагин отображения). Затем я могу использовать этот снимок, чтобы быстро определить, изменилась ли модель.

То, что я ищу, это некоторая функция общего назначения, которая может возвращать объект, который представляет различия между двумя объектами JavaScript, где один из объектов считается мастером.

Например, предположим, что это мой снимок:

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

Затем предположим, что данные в реальном времени выглядят следующим образом:

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

Мне нужна функция утилиты .getChanges(snapShot, liveData), которая возвращает следующее:

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

Я надеялся, что библиотека _. underscore может иметь что-то подобное, но я не мог найти ничего похожего на это.

Ответы

Ответ 1

Я не думаю, что есть такая функция в подчеркивании, но ее легко реализовать:

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

или

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
}

Это не будет работать с массивами (или любыми другими не-равными объектами) или иначе структурированными объектами (удаление, примитив к изменениям типа объекта).

Ответ 2

Опубликуйте свой собственный ответ, чтобы люди могли увидеть окончательную реализацию, которая также работает с массивами. В приведенном ниже коде "um" - это мое пространство имен, и я также использую методы _.isArray() и _.isObject из Underscore.js.

Код, который ищет "_KO", используется для пропуска мимо элементов Knockout.js, которые присутствуют в объекте.

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

Ответ 3

Я использую JsonDiffPatch в моих проектах, чтобы найти дельту, перенести ее по сети, а затем исправить объект на другом чтобы получить точную копию. Он очень прост в использовании и работает очень хорошо.

И он также работает с массивами!

Ответ 4

Решение с использованием jQuery.isEmptyObject()

Это перезагружает решение getChanges (prev, now) выше. Он должен работать и на объектах разного типа, а также когда prev [prop] равен undefined (в результате теперь [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;
}

Ответ 5

Не нужно использовать jquery для этого. Недавно я написал модуль для этого: https://github.com/Tixit/odiff. Вот пример:

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}]}
]
*/

В readme для odiff я перечислил другие модули, которые, как я определил, не соответствовали моим потребностям, если вы тоже хотите проверить их.

Ответ 6

На основании вышеприведенного решения. Эта версия исправляет ошибку, некоторые изменения не обнаруживаются с исходным решением.

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

Ответ 7

Помните, что typeof now[prop] == "object" равно true, если now[prop] - дата.

Вместо: typeof now[prop] == "object"

Я использовал: Object.prototype.toString.call( now[prop] === "[object Object]")