Общий глубокий дифференциал между двумя объектами - PullRequest
181 голосов
/ 20 декабря 2011

У меня есть два объекта: oldObj и newObj.

Данные в oldObj использовались для заполнения формы, а newObj является результатом изменения пользователем данных в этой форме и их отправки.

Оба объекта глубокие, т.е. у них есть свойства, которые являются объектами или массивами объектов и т. д. - они могут иметь глубину n уровней, поэтому алгоритм diff должен быть рекурсивным.

Теперь мне нужно не просто выяснить, что было изменено (как добавлено / обновлено / удалено) с oldObj на newObj, но и как лучше всего это представить.

До сих пор я думал только о том, чтобы создать genericDeepDiffBetweenObjects метод, который бы возвращал объект в форме {add:{...},upd:{...},del:{...}}, но потом я подумал: кому-то другому это могло понадобиться раньше.

Итак ... кто-нибудь знает библиотеку или фрагмент кода, который сделает это и, возможно, найдет еще лучший способ представить разницу (способом, который все еще сериализуем в JSON)?

Обновление:

Я подумал о лучшем способе представления обновленных данных, используя ту же структуру объекта, что и newObj, но превратив все значения свойств в объекты в форме:

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

Так что если newObj.prop1 = 'new value' и oldObj.prop1 = 'old value', то будет установлено returnObj.prop1 = {type: 'update', data: 'new value'}

Обновление 2:

Когда мы получаем свойства, являющиеся массивами, он становится действительно волосатым, поскольку массив [1,2,3] следует считать равным [2,3,1], что достаточно просто для массивов типов, основанных на значениях, таких как string, int & bool, но становится действительно трудно справиться, когда дело доходит до массивов ссылочных типов, таких как объекты и массивы.

Пример массивов, которые должны быть равны:

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

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

Ответы [ 14 ]

117 голосов
/ 22 декабря 2011

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

Единственное, что отличается от вашего предложения, это то, что я не считаю [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]] быть тем же, потому что я думаю, что массивы не равны, если порядок их элементов не одинаков.Конечно, это можно изменить при необходимости.Также этот код может быть дополнительно расширен, чтобы принимать функцию в качестве аргумента, которая будет использоваться для произвольного форматирования объекта diff на основе переданных значений примитивов (теперь эта работа выполняется методом «CompareValues»).

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);
81 голосов
/ 03 сентября 2014

Используя Underscore, простой 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; })

Результаты в частях o1, которые соответствуют, но с другими значениями в o2:

{a: 1, b: 2}

Это было бы иначе для глубокого различия:

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

Как отметил @Juhana в комментариях, приведенное выше является только diff a -> b и не обратимым (что означает, что дополнительные свойства в b будут игнорироваться). Используйте вместо 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());

См. http://jsfiddle.net/drzaus/9g5qoxwj/ для полного примера + тесты + миксины

37 голосов
/ 23 мая 2016

Я хотел бы предложить решение ES6 ... Это односторонняя разность, означающая, что он будет возвращать ключи / значения из o2, которые не идентичны их аналогам в 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]
  }
}, {})
21 голосов
/ 23 июня 2015

Использование 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);
    }
});

Я не использую ключ / объект / источник, но я оставил его там, если вам нужен доступ к ним.Сравнение объектов просто не позволяет консоли печатать различия для консоли от самого внешнего элемента до самого внутреннего элемента.

Вы можете добавить некоторую логику внутри для обработки массивов.Возможно, сначала отсортируйте массивы.Это очень гибкое решение.

EDIT

Изменено с _.merge на _.mergeWith из-за обновления lodash.Спасибо Aviron за то, что обратили внимание на изменение.

8 голосов
/ 22 июля 2015

Вот библиотека JavaScript, которую вы можете использовать для поиска различий между двумя объектами JavaScript:

URL-адрес Github: https://github.com/cosmicanant/recursive-diff

URL-адрес Npmjs: https://www.npmjs.com/package/recursive-diff

recursiveDiff

Вы можете использовать библиотеку recursive-diff в браузере, а также в Node.js.Для браузера:

8 голосов
/ 06 июля 2015

В наши дни для этого доступно немало модулей.Я недавно написал модуль для этого, потому что я не был удовлетворен многочисленными модулями сравнения, которые я нашел.Его называют odiff: https://github.com/Tixit/odiff.Я также перечислил несколько наиболее популярных модулей и почему они не были приняты в файле readme odiff, который вы можете просмотреть, если у 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}]}
]
*/
2 голосов
/ 12 июля 2018

с использованием lodash.

function difference(object, base) {
    function changes(object, base) {
        return _.transform(object, function(result, value, key) {
            if (!_.isEqual(value, base[key])) {
                result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value;
            }
        });
    }
    return changes(object, base);
}
2 голосов
/ 12 июля 2016

Я разработал функцию с именем «CompareValue ()» в Javascript.он возвращает, является ли значение тем же или нет.Я вызвал CompareValue () для цикла одного объекта.Вы можете получить разницу двух объектов в 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);
2 голосов
/ 21 декабря 2011

Я использовал этот фрагмент кода для выполнения описанной вами задачи:

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

это даст вам новый объект, который объединит все изменения между старым объектом и новым объектом из вашей формы

1 голос
/ 19 января 2018

Я просто использую ramda, для решения той же проблемы, мне нужно знать, что изменилось в новом объекте. Так вот мой дизайн.

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)

Результат: название объекта и его статус.

[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...