Differenza profonda generica tra due oggetti

Ho due oggetti: oldObj e newObj .

I dati di oldObj stati utilizzati per popolare un modulo e newObj è il risultato della modifica dei dati dell’utente in questo modulo e dell’invio.

Entrambi gli oggetti sono profondi, cioè. hanno proprietà che sono oggetti o matrici di oggetti ecc. – possono essere n livelli profondi, quindi l’algoritmo diff deve essere ricorsivo.

Ora ho bisogno non solo di capire cosa è stato modificato (come aggiunto / aggiornato / cancellato) da oldObj a newObj , ma anche come rappresentarlo al meglio.

Fino ad ora genericDeepDiffBetweenObjects solo a build un metodo genericDeepDiffBetweenObjects che restituisse un object sul form {add:{...},upd:{...},del:{...}} ma poi ho pensato: qualcuno altro deve aver avuto bisogno di questo prima.

Quindi … qualcuno sa di una libreria o di un pezzo di codice che lo farà e forse avrà un modo ancora migliore di rappresentare la differenza (in un modo che è ancora serializzabile in JSON)?

Aggiornare:

Ho pensato a un modo migliore per rappresentare i dati aggiornati, utilizzando la stessa struttura di oggetti di newObj , ma trasformando tutti i valori delle proprietà in oggetti nel modulo:

 {type: '', data: } 

Quindi se newObj.prop1 = 'new value' e oldObj.prop1 = 'old value' imposterebbe returnObj.prop1 = {type: 'update', data: 'new value'}

Aggiornamento 2:

Diventa davvero peloso quando arriviamo alle proprietà che sono array, poiché l’array [1,2,3] dovrebbe essere contato come uguale a [2,3,1] , che è abbastanza semplice per gli array di tipi basati sul valore come string, int & bool, ma diventa davvero difficile da gestire quando si tratta di matrici di tipi di riferimento come oggetti e matrici.

Array di esempio che dovrebbero essere trovati uguali:

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

Non solo è piuttosto complicato verificare questo tipo di uguaglianza dei valori profondi, ma anche capire un buon modo per rappresentare i cambiamenti che potrebbero essere.

Ho scritto una piccola lezione che sta facendo quello che vuoi, puoi testarlo qui .

L’unica cosa che è diversa dalla tua proposta è che non considero [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]] per essere lo stesso, perché penso che gli array non siano uguali se l’ordine dei loro elementi non è lo stesso. Ovviamente questo può essere cambiato se necessario. Anche questo codice può essere ulteriormente migliorato per assumere la funzione di argomento che verrà utilizzato per formattare l’object diff in modo arbitrario in base ai valori primitivi passati (ora questo lavoro viene eseguito dal metodo “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); 

Utilizzando Underscore, una semplice 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; }) 

Risultati nelle parti di o1 che corrispondono ma con valori diversi in o2 :

 {a: 1, b: 2} 

Sarebbe diverso per una diff profonda:

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

Come sottolineato da @Juhana nei commenti, quanto sopra è solo un diff a -> b e non reversibile (il che significa che le proprietà extra in b sarebbero ignorate). Usa invece 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()); 

Vedi http://jsfiddle.net/drzaus/9g5qoxwj/ per l’esempio completo + test + mixins

Mi piacerebbe offrire una soluzione ES6 … Questa è una diff unidirezionale, il che significa che restituirà chiavi / valori da o2 che non sono identici alle loro controparti in 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 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); } }); 

Ecco una libreria JavaScript che puoi utilizzare per trovare la differenza tra due oggetti JavaScript:

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

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

recursiveDiff

È ansible utilizzare la libreria ricorsiva-diff in browser e Node.js. Per il browser, effettuare le seguenti operazioni:

   

Mentre su node.js puoi richiedere il modulo 'ricorsive-diff' e usarlo come di seguito:

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

In questi giorni, ci sono alcuni moduli disponibili per questo. Recentemente ho scritto un modulo per farlo, perché non ero soddisfatto dei numerosi moduli diffusi che ho trovato. Si chiama odiff : https://github.com/Tixit/odiff . Ho anche elencato alcuni dei moduli più popolari e perché non erano accettabili nel readme di odiff , che potreste dare un’occhiata se odiff non ha le proprietà che volete. Ecco un esempio:

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

Ho usato questo pezzo di codice per eseguire l’attività che descrivi:

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

questo ti porterà un nuovo object che unirà tutte le modifiche tra il vecchio object e il nuovo object dal tuo modulo

Ho sviluppato la funzione denominata “compareValue ()” in Javascript. restituisce se il valore è uguale o no. Ho chiamato compareValue () nel ciclo for di un object. puoi ottenere la differenza di due oggetti in 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); 

Io uso solo ramda, per risolvere lo stesso problema, ho bisogno di sapere cosa è cambiato nel nuovo object. Quindi qui il mio 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) 

il risultato è, il nome della proprietà e il suo stato.

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

Ho già scritto una funzione per uno dei miei progetti che confronterà un object come opzioni utente con il suo clone interno. Può anche validare e persino sostituire i valori predefiniti se l’utente ha inserito un tipo di dati errato o rimosso, in puro javascript.

In IE8 funziona al 100%. Testato con successo.

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

/ * risultato

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

La funzione più estesa e semplificata dalla risposta di sbgoran.
Ciò consente una scansione profonda e trova la similitudine di un array.

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

So di essere in ritardo per la festa, ma avevo bisogno di qualcosa di simile che le risposte di cui sopra non aiutassero.

Stavo usando la funzione $ watch di Angular per rilevare i cambiamenti in una variabile. Non solo avevo bisogno di sapere se una proprietà era cambiata sulla variabile, ma volevo anche assicurarmi che la proprietà che era cambiata non fosse un campo temporaneo e calcolato. In altre parole, volevo ignorare certe proprietà.

Ecco il codice: https://jsfiddle.net/rv01x6jo/

Ecco come usarlo:

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

Spero che questo aiuti qualcuno.