Diferença profunda genérica entre dois objects

Eu tenho dois objects: oldObj e newObj .

Os dados em oldObj foram usados ​​para preencher um formulário e newObj é o resultado do usuário alterar dados neste formulário e enviá-lo.

Ambos os objects são profundos, ie. eles têm propriedades que são objects ou matrizes de objects, etc. – eles podem ter n níveis profundos, portanto, o algoritmo diff precisa ser recursivo.

Agora eu preciso não apenas descobrir o que foi alterado (como adicionado / atualizado / excluído) de oldObj para newObj , mas também como melhor representá-lo.

Até agora, meu pensamento era apenas criar um método genericDeepDiffBetweenObjects que retornaria um object no formulário {add:{...},upd:{...},del:{...}} mas depois pensei: alguém mais deve ter precisado disso antes.

Então … alguém sabe de uma biblioteca ou um pedaço de código que fará isso e talvez tenha uma maneira ainda melhor de representar a diferença (de uma maneira que ainda é serializada pelo JSON)?

Atualizar:

Eu pensei em uma maneira melhor para representar os dados atualizados, usando a mesma estrutura de object como newObj , mas transformando todos os valores de propriedade em objects no formulário:

 {type: '', data: } 

Portanto, se newObj.prop1 = 'new value' e oldObj.prop1 = 'old value' ele deve definir returnObj.prop1 = {type: 'update', data: 'new value'}

Atualização 2:

Ele fica realmente cabeludo quando chegamos a propriedades que são matrizes, uma vez que a matriz [1,2,3] deve ser contada como igual a [2,3,1] , o que é simples o suficiente para matrizes de tipos baseados em valor, como string, int & bool, mas fica realmente difícil de lidar quando se trata de arrays de tipos de referência, como objects e matrizes.

Exemplos de matrizes que devem ser encontrados iguais:

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

Não só é bastante complexo verificar esse tipo de igualdade de valor profundo, mas também descobrir uma boa maneira de representar as mudanças que podem ser.

Eu escrevi uma pequena aula que está fazendo o que você quer, você pode testá-lo aqui .

A única coisa que é diferente da sua proposta é que eu não considero [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]] para ser o mesmo, porque eu acho que as matrizes não são iguais se a ordem de seus elementos não é a mesma. Claro que isso pode ser alterado, se necessário. Além disso, este código pode ser melhorado para ter a function como argumento que será usado para formatar o object diff de maneira arbitrária com base nos valores primitivos passados ​​(agora este trabalho é feito pelo método “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); 

Usando Underscore, um diff simples:

 var o1 = {a: 1, b: 2, c: 2}, o2 = {a: 2, b: 1, c: 2}; _.omit(o1, function(v,k) { return o2[k] === v; }) 

Resulta nas partes de o1 que correspondem mas com valores diferentes em o2 :

 {a: 1, b: 2} 

Seria diferente para um diff profundo:

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

Como apontado por @Juhana nos comentários, o acima é apenas um diff a -> b e não reversível (o que significa que propriedades extras em b seriam ignoradas). Use em vez disso 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()); 

Veja http://jsfiddle.net/drzaus/9g5qoxwj/ para o exemplo completo + testes + mixins

Eu gostaria de oferecer uma solução ES6 … Este é um diff unidirecional, o que significa que ele retornará chaves / valores de o2 que não são idênticos aos seus equivalentes no 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] } }, {}) 

Usando o 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); } }); 

Aqui está uma biblioteca JavaScript que você pode usar para encontrar diferenças entre dois objects JavaScript:

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

URL do NPMjs: https://www.npmjs.com/package/recursive-diff

recursiveDiff

Você pode usar a biblioteca recursive-diff no navegador, bem como o Node.js. Para o navegador, faça o seguinte:

   

Considerando que em node.js você pode exigir o módulo 'recursive-diff' e usá-lo como abaixo:

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

Hoje em dia, existem alguns módulos disponíveis para isso. Recentemente, escrevi um módulo para fazer isso, porque não estava satisfeito com os numerosos módulos de comparação que encontrei. Seu odiff chamado: https://github.com/Tixit/odiff . Eu também listei alguns dos módulos mais populares e por que eles não eram aceitáveis ​​no readme do odiff , que você poderia dar uma olhada se odiff não tivesse as propriedades que você queria. Aqui está um exemplo:

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

Eu usei este pedaço de código para fazer a tarefa que você descreve:

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

isso te dará um novo object que irá mesclar todas as mudanças entre o object antigo e o novo object do seu formulário

Eu desenvolvi a function chamada “compareValue ()” em JavaScript. ele retorna se o valor é igual ou não. Eu chamei compareValue () em loop de um object. você pode obter diferença de dois objects em 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); 

Eu só uso ramda, para resolver o mesmo problema, eu preciso saber o que é alterado no novo object. Então aqui meu design.

 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) 

O resultado é o nome da propriedade e seu status.

 [{"id":true}, {"name":false}, {"secondName":true}, {"age":false}] 

Eu já escrevi uma function para um dos meus projetos que irá comparar um object como opções do usuário com seu clone interno. Ele também pode validar e até mesmo replace por valores padrão se o usuário inseriu um tipo incorreto de dados ou foi removido, em um javascript puro.

No IE8 100% funciona. Testado com sucesso.

 // 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(/^[az][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 )) //); 

/ * resultado

 { "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" } */ 

A function mais estendida e simplificada da resposta do sbgoran.
Isso permite uma varredura profunda e a similitude de uma matriz.

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

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

Eu sei que estou atrasado para a festa, mas eu precisava de algo semelhante que as respostas acima não ajudassem.

Eu estava usando a function $ watch do Angular para detectar mudanças em uma variável. Não precisei saber apenas se uma propriedade havia sido alterada na variável, mas também queria ter certeza de que a propriedade que mudou não era um campo calculado e temporário. Em outras palavras, eu queria ignorar certas propriedades.

Aqui está o código: https://jsfiddle.net/rv01x6jo/

Veja como usá-lo:

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

Espero que isso ajude alguém.