Общий глубокий дифференциал между двумя объектами


223

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

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

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

Теперь мне нужно не только выяснить, что было изменено (как добавлено / обновлено / удалено) из oldObjto 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]]

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



2
@ a'r: это не дубликат stackoverflow.com/questions/1200562/… - я знаю, как обходить объекты, я ищу предшествующий уровень техники, поскольку это не тривиально и для его реализации потребуется реальное время, и я лучше использовать библиотеку, чем делать ее с нуля.
Мартин Йесперсен

1
Вы действительно нуждаетесь в diff объектов, это newObj, сгенерированный с сервера при отправке формы ответа? Потому что, если у вас нет «серверных обновлений» объекта, вы можете упростить вашу проблему, подключив соответствующие прослушиватели событий, и при взаимодействии с пользователем (изменение объекта) вы можете обновить / сгенерировать список желаемых изменений.
sbgoran

1
@sbgoran: newObjгенерируется js-кодом для чтения значений из формы в DOM. Есть несколько способов сохранить состояние и сделать это намного проще, но я бы хотел оставить его без сохранения состояния в качестве упражнения. Также я ищу предшествующий уровень техники, чтобы увидеть, как другие могли бы справиться с этим, если действительно кто-то есть.
Мартин Йесперсен

3
Вот очень сложная библиотека для сравнения / исправления любой пары объектов Javascript. github.com/benjamine/jsondiffpatch, вы можете увидеть ее вживую здесь: benjamine.github.io/jsondiffpatch/demo/index.html (отказ от ответственности: я автор)
Беня

Ответы:


142

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

Единственное, что отличается от вашего предложения, это то, что я не считаю [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);


3
+1 Это неплохой кусок кода. Однако есть ошибка (проверьте этот пример: jsfiddle.net/kySNu/3 c создается как, undefinedно должна быть строкой 'i am created'), и, кроме того, он не делает то, что мне нужно, так как ему не хватает сравнения значений глубокого массива, которое является самая важная (и сложная / трудная) часть. Как примечание стороны, конструкция 'array' != typeof(obj)бесполезна, так как массивы - это объекты, которые являются экземплярами массивов.
Мартин Йесперсен

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

1
И как вы имеете в виду «отсутствие глубокого сравнения значений массива» для массивов, которые вы получите для каждого индекса этого {type: ..., data:..}объекта. Чего не хватает, так это поиска значения из первого массива за секунду, но, как я уже упоминал в своем ответе, я не думаю, что массивы равны, если порядок их значений не одинаков ( [1, 2, 3] is not equal to [3, 2, 1]на мой взгляд).
sbgoran

6
@MartinJespersen КИ, как бы вы в общей относиться к этим массивам , то: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Теперь первый объект в первом массиве обновлен значениями «value1» или «value2». И это простой пример, он может быть намного сложнее с глубоким вложением. Если вы хотите / нужно глубоко вложенность сравнение независимо от положения ключа не создавать массивы объектов, создавать объекты с вложенными объектами , как для предыдущего примера: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
Сбгоран

2
Я согласен с вашей последней точкой зрения - исходная структура данных должна быть изменена на что-то, что было бы легче сделать на самом деле. Поздравляю, вы прибили это :)
Мартин Йесперсен

88

Используя 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/ для полного примера + тесты + миксины


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

2
@ Сейрия ненавидит ненавидеть, я думаю ... Я сделал и то, и другое, потому что я изначально думал, omitчто это будет глубокий провал, но был неправ, поэтому включил и для сравнения.
drzaus

1
Хорошее решение. Я хотел бы предложить , чтобы изменить r[k] = ... : vв r[k] = ... : {'a':v, 'b':b[k] }, таким образом , вы можете увидеть два значения.
Guyaloni

2
Оба из них возвращают ложный отрицательный результат, когда объекты в остальном идентичны, но второй имеет больше элементов, например, {a:1, b:2}и {a:1, b:2, c:3}.
JJJ

1
Это должно быть _.omitByвместо _.omit.
JP

48

Я хотел бы предложить решение 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]
  }
}, {})

3
Хорошее решение, но вы можете проверить эту if(o1[key] === o1[key])строку, чувак
bm_i

Код завершен? Я получаюUncaught SyntaxError: Unexpected token ...
Seano

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

3
Да, это не рекурсивно @Spurious
Немесариал

2
Просто имейте в виду, что с этим решением для каждого элемента в объекте вы получаете совершенно новый объект, построенный со всеми существующими элементами, скопированными в него, просто чтобы добавить один элемент в массив. Для небольших объектов это хорошо, но для более крупных объектов оно будет экспоненциально замедляться.
Malvineous

22

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

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

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

РЕДАКТИРОВАТЬ

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


6
В lodash 4.15.0 функция _.merge с настройщиком больше не поддерживается, поэтому вместо нее следует использовать _.mergeWith.
Авиран Коэн

1
эта функция великолепна, но не работает во вложенном объекте.
Джо Аллен

14

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

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

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

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

<script type="text" src="https://unpkg.com/recursive-diff@1.0.0/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>

Принимая во внимание, что в node.js вам может потребоваться модуль 'recursive-diff' и использовать его, как показано ниже:

const diff = require('recursive-diff');
const ob1 = {a: 1}, ob2: {b:2};
const diff = diff.getDiff(ob1, ob2);

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

добавлена ​​дата поддержки
Anant

9

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

7
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

Существует модуль npm с более чем 500 тыс. Загрузок в неделю: https://www.npmjs.com/package/deep-object-diff

Мне нравится объект как представление различий - особенно легко увидеть структуру, когда она сформирована.

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

2

Я использовал этот кусок кода для выполнения задачи, которую вы описываете:

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
Я использую фреймворк Ext здесь, но вы можете заменить его и использовать
любой

Объединение объектов тривиально и может быть сделано так же просто, как $.extend(true,obj1,obj2)с помощью jQuery. Это совсем не то, что мне нужно. Мне нужна разница между двумя объектами, а не их комбинация.
Мартин Йесперсен

здорово, что здесь используется Ext
перекись

2

Я разработал функцию с именем «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);


1

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

Я использовал функцию $ watch Angular для обнаружения изменений в переменной. Мне нужно было не только узнать, изменилось ли свойство переменной, но я также хотел убедиться, что измененное свойство не было временным вычисляемым полем. Другими словами, я хотел игнорировать определенные свойства.

Вот код: https://jsfiddle.net/rv01x6jo/

Вот как это использовать:

// 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]);

Надеюсь, это кому-нибудь поможет.


Пожалуйста, включите код в свой ответ, а не просто скрипку.
xpy

Кажется, что defineProperty решил бы эту проблему с лучшей производительностью, если я правильно помню, он работает вплоть до IE9.
Питер

Спасибо..!! Ваш код работает как шарм и спас мой день. У меня объект json из 1250 строк, и он дает мне точное значение o / p, которое я хочу.
Теджас Мехта

1

Я просто использую 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}]

1

Вот машинописная версия кода @sbgoran

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;

  }
}

1

Вот модифицированная версия чего-то найденного на 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;
        }
    });
}

1

Я изменил ответ @ sbgoran так, чтобы результирующий объект diff включал только измененные значения и пропускал значения, которые были одинаковыми. Кроме того, он показывает как исходное значение, так и обновленное значение .

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

0

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

В IE8 100% работает. Проверено успешно.

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

/ * результат

{
  "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

Более расширенная и упрощенная функция из ответа sbgoran.
Это позволяет глубокое сканирование и найти сходство массива.

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


0

Я наткнулся здесь, пытаясь найти способ получить разницу между двумя объектами. Это мое решение с использованием 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

Фрагмент кода ниже:

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>


0

Я взял ответ выше @sbgoran и изменил его для своего случая так же, как и вопрос, необходимый для обработки массивов как наборов (т.е. порядок не важен для 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);
  }
 };
}();

0

Вот решение, которое:

  • Typescript (но легко конвертируемый в Javascript)
  • не имеют либ-зависимостей
  • универсальный и не заботится о проверке типов объектов (кроме objectтипа)
  • поддерживает свойства со значением undefined
  • глубоко не (по умолчанию)

Сначала мы определяем интерфейс результата сравнения:

export interface ObjectComparison {
  added: {};
  updated: {
    [propName: string]: Change;
  };
  removed: {};
  unchanged: {};
}

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

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

Тогда мы можем предоставить diff функцию, которая является просто двумя циклами (с рекурсивностью, если deepесть true):

export class ObjectUtils {

  static diff(o1: {}, o2: {}, deep = false): ObjectComparison {
    const added = {};
    const updated = {};
    const removed = {};
    const unchanged = {};
    for (const prop in o1) {
      if (o1.hasOwnProperty(prop)) {
        const o2PropValue = o2[prop];
        const o1PropValue = o1[prop];
        if (o2.hasOwnProperty(prop)) {
          if (o2PropValue === o1PropValue) {
            unchanged[prop] = o1PropValue;
          } else {
            updated[prop] = deep && this.isObject(o1PropValue) && this.isObject(o2PropValue) ? this.diff(o1PropValue, o2PropValue, deep) : {newValue: o2PropValue};
          }
        } else {
          removed[prop] = o1PropValue;
        }
      }
    }
    for (const prop in o2) {
      if (o2.hasOwnProperty(prop)) {
        const o1PropValue = o1[prop];
        const o2PropValue = o2[prop];
        if (o1.hasOwnProperty(prop)) {
          if (o1PropValue !== o2PropValue) {
            if (!deep || !this.isObject(o1PropValue)) {
              updated[prop].oldValue = o1PropValue;
            }
          }
        } else {
          added[prop] = o2PropValue;
        }
      }
    }
    return { added, updated, removed, unchanged };
  }

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

В качестве примера звоним:

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'
  },
);

вернется:

{
  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'},
}

и вызов того же с deepтретьим параметром вернет:

{
  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'},
}

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.