Общая глубокая разница между двумя объектами
У меня есть два объекта: 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, но становится очень трудно обрабатывать, когда речь заходит о массивах ссылочных типов, таких как objects и bool. массивы.
Пример массивов, которые должны быть найдены равными:
[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]
Не только довольно сложно проверить этот тип глубокого равенства значений, но и найти хороший способ представления возможных изменений.
13 ответов:
Я написал небольшой класс, который делает то, что вы хотите, вы можете проверить его здесь.
Единственное, что отличается от вашего предложения, это то, что я не считаю[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 ('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; }, 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 ('undefined' == typeof(value1)) { return this.VALUE_CREATED; } if ('undefined' == typeof(value2)) { return this.VALUE_DELETED; } return this.VALUE_UPDATED; }, isFunction: function(obj) { return {}.toString.apply(obj) === '[object Function]'; }, isArray: function(obj) { return {}.toString.apply(obj) === '[object Array]'; }, isDate: function(obj) { return {}.toString.apply(obj) === '[object Date]'; }, isObject: function(obj) { return {}.toString.apply(obj) === '[object Object]'; }, isValue: function(obj) { return !this.isObject(obj) && !this.isArray(obj); } } }(); 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);
Используя подчеркивание, простое различие:
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 в комментариях, вышеизложенное является только различием 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/ для полного примера+тесты+миксины
Я хотел бы предложить решение 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] } }, {})
Используя Лодаш:
_.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
Изменено с _.слиться с _.mergeWith из-за обновления lodash. Спасибо Авирону, что заметил перемену.
Вот библиотека JavaScript, которую вы можете использовать для поиска различий между двумя объектами JavaScript:
Url Github: https://github.com/cosmicanant/recursive-diff
Npmjs url: https://www.npmjs.com/package/recursive-diff
Вы можете использовать библиотеку recursive-diff как в браузере, так и в узле.JS. Для браузера выполните следующие действия:
<script type="text" src="index.js"/> <script type="text/javascript"> var ob1 = {a:1, b: [2,3]}; var ob2 = {a:2, b: [3,3,1]}; var delta = diff.getDiff(ob1,ob2); /* console.log(delta) will dump following data { '/a':{operation:'update',value:2} '/b/0':{operation:'update',value:3}, '/b/2':{operation:'add',value:1}, } */ var ob3 = diff.applyDiff(ob1, delta); //expect ob3 is deep equal to ob2 </script>Тогда как в узле.js вы можете потребовать модуль 'recursive-diff' и использовать его как показано ниже:
var diff = require('recursive-diff'); var ob1 = {a: 1}, ob2: {b:2}; var diff = diff.getDiff(ob1, ob2);
В наши дни для этого доступно довольно много модулей. Я недавно написал модуль, чтобы сделать это, потому что я не был удовлетворен многочисленными различными модулями, которые я нашел. Его называют
odiff: https://github.com/Tixit/odiff . Я также перечислил кучу самых популярных модулей и почему они не были приемлемы в readmeodiff, который вы можете просмотреть, если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}]} ] */
Я использовал этот фрагмент кода для выполнения задачи, которую вы описываете:
Это даст вам новый объект, который объединит все изменения между старым объектом и новым объектом из вашей формы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; }
Я разработал функцию с именем compareValue () в Javascript. он возвращает независимо от того, является ли значение одинаковым или нет. Я вызвал compareValue () в цикле for для одного объекта. вы можете получить разницу двух объектов в 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);
Я просто использую рамду, для решения той же проблемы мне нужно знать, что изменилось в новом объекте. Так вот мой замысел.
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}]
Я уже написал функцию для одного из своих проектов, которая будет сравнивать объект в качестве пользовательских опций с его внутренним клоном. Он также может проверять и даже заменять значения по умолчанию, если пользователь ввел плохой тип данных или удалил, в чистом 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" } */
Более расширенная и упрощенная функция из ответа сбгорана.
Это позволяет глубоко сканировать и находить сходство массива.
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'; }
Использование лодаша.
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); }
Я знаю, что опаздываю на вечеринку, но мне нужно было что-то подобное, что вышеприведенные ответы не помогли.
Я использовал функцию $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]);Надеюсь, это кому-то поможет.

Comments