Differenza profonda generica tra due oggetti


223

Ho due oggetti: oldObjenewObj .

I dati in sono oldObjstati utilizzati per popolare un modulo ed newObjè il risultato dell'utente che modifica i dati in questo modulo e li invia.

Entrambi gli oggetti sono profondi, vale a dire. hanno proprietà che sono oggetti o matrici di oggetti ecc. - possono avere n livelli di profondità, quindi l'algoritmo diff deve essere ricorsivo.

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

Finora il mio pensiero era solo quello di costruire un genericDeepDiffBetweenObjectsmetodo che avrebbe restituito un oggetto sul modulo, {add:{...},upd:{...},del:{...}}ma poi ho pensato: qualcun altro doveva averne bisogno prima.

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

Aggiornare:

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

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

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

Aggiornamento 2:

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

Matrici di esempio che dovrebbero essere trovate uguali:

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

Non solo è abbastanza complesso verificare questo tipo di uguaglianza di valore profondo, ma anche trovare un buon modo per rappresentare i cambiamenti che potrebbero essere.



2
@ a'r: Non è un duplicato di stackoverflow.com/questions/1200562/… - So attraversare gli oggetti, cerco l'arte nota poiché non è banale e ci vorrà del tempo per implementarla, e io preferirei usare una libreria piuttosto che farla da zero.
Martin Jespersen,

1
Hai davvero bisogno di diff di oggetti, newObj è generato dal server sul modulo di risposta? Perché se non si dispone di "aggiornamenti del server" di un oggetto, è possibile semplificare il problema collegando listener di eventi appropriati e all'interazione dell'utente (modifica dell'oggetto) è possibile aggiornare / generare l'elenco di modifiche desiderate.
sbgoran,

1
@sbgoran: newObjviene generato dal codice js che legge i valori da un modulo nel DOM. Esistono diversi modi per mantenere lo stato e farlo molto più facilmente, ma mi piacerebbe mantenerlo apolide come esercizio. Inoltre sto cercando arte nota per vedere come altri avrebbero potuto affrontare questo, se davvero qualcuno lo ha fatto.
Martin Jespersen,

3
ecco una libreria molto sofisticata per diff / patchare qualsiasi coppia di oggetti Javascript github.com/benjamine/jsondiffpatch puoi vederlo dal vivo qui: benjamine.github.io/jsondiffpatch/demo/index.html (disclaimer: sono l'autore)
Benja

Risposte:


142

Ho scritto una piccola classe che sta facendo quello che vuoi, puoi provarlo qui .

L'unica cosa diversa dalla tua proposta è che non considero la [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]stessa cosa, perché penso che le matrici non siano uguali se l'ordine dei loro elementi non è lo stesso. Naturalmente questo può essere modificato se necessario. Anche questo codice può essere ulteriormente migliorato per assumere la funzione di argomento che verrà utilizzato per formattare l'oggetto diff in modo arbitrario in base ai valori primitivi passati (ora questo lavoro viene eseguito con il 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 (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 Non è un brutto codice. C'è comunque un bug (guarda questo esempio: jsfiddle.net/kySNu/3 c è stato creato come undefinedma dovrebbe essere la stringa 'i am created'), e inoltre non fa ciò di cui ho bisogno poiché manca il valore di array profondo confronta quale è il la parte più cruciale (e complessa / difficile). Come nota a margine, il costrutto 'array' != typeof(obj)è inutile poiché le matrici sono oggetti che sono istanze di matrici.
Martin Jespersen,

1
Ho aggiornato il codice, ma non sono sicuro del valore desiderato nell'oggetto risultante, in questo momento il codice restituisce il valore dal primo oggetto e se non esiste il valore dal secondo verrà impostato come dati.
sbgoran,

1
E come intendi "mancanza del confronto del valore di array profondo" per gli array che otterrai per ogni indice di {type: ..., data:..}quell'oggetto. Ciò che manca è la ricerca del valore dal primo array al secondo, ma come ho già detto nella mia risposta, non credo che gli array siano uguali se l'ordine dei loro valori non è uguale ( [1, 2, 3] is not equal to [3, 2, 1]secondo me).
sbgoran,

6
@MartinJespersen OK, come ti genericamente il trattamento di questa array poi: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Ora il primo oggetto nel primo array viene aggiornato con "valore1" o "valore2". E questo è un semplice esempio, potrebbe essere molto complicato con l'annidamento profondo. Se si desidera / bisogno di confronto profondo nidificazione indipendentemente dalla posizione della chiave non creano array di oggetti, creare oggetti con oggetti nidificati, come ad esempio precedente: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran,

2
Concordo con te sull'ultimo punto di vista: la struttura dei dati originale dovrebbe essere cambiata in qualcosa che è più facile fare una vera differenza. Complimenti, l'hai inchiodato :)
Martin Jespersen,

88

Usando Underscore, un 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; })

I risultati nelle parti di o1ciò corrispondono ma con valori diversi in o2:

{a: 1, b: 2}

Sarebbe diverso per un diff profondo:

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 verrebbero 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 esempio completo + test + mixins


Non sono sicuro del motivo per cui hai ottenuto il downgrade, questo è stato sufficiente in quanto hai fornito un esempio superficiale, semplice e una funzione profonda più complessa.
Seiyria,

2
Gli odiatori di @Seiyria odieranno, immagino ... Ho fatto entrambe le cose perché inizialmente pensavo omitche fosse un diff profondo, ma era sbagliato, quindi incluso anche per il confronto.
drzaus,

1
Bella soluzione. Vorrei suggerire di cambiare r[k] = ... : vin r[k] = ... : {'a':v, 'b':b[k] }questo modo si può vedere due valori.
Guyaloni,

2
Entrambi questi restituiscono un falso negativo quando gli oggetti sono altrimenti identiche ma la seconda ha più elementi, ad esempio {a:1, b:2}e {a:1, b:2, c:3}.
JJJ,

1
Dovrebbe essere _.omitByinvece di _.omit.
JP

48

Vorrei offrire una soluzione ES6 ... Questa è una differenza unidirezionale, il che significa che restituirà chiavi / valori o2che 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]
  }
}, {})

3
Bella soluzione ma potresti voler controllare quella if(o1[key] === o1[key])linea amico
bm_i

Il codice è completo? RicevoUncaught SyntaxError: Unexpected token ...
Seano

2
Mi piace la soluzione ma ha un problema, se l'oggetto è più profondo di un livello, restituirà tutti i valori negli oggetti nidificati modificati - o almeno quello che sta succedendo per me.
Spurio

3
Sì, questo non è ricorsivo @Spurious
Nemesarial

2
Basta tenere presente che con questa soluzione, per ogni elemento nell'oggetto si ottiene un oggetto completamente nuovo costruito con tutti gli elementi esistenti copiati in esso solo per aggiungere un elemento all'array. Per piccoli oggetti va bene, ma rallenterà in modo esponenziale per oggetti più grandi.
Malvineous,

22

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

Non uso chiave / oggetto / sorgente ma l'ho lasciato lì se è necessario accedervi. Il confronto degli oggetti impedisce alla console di stampare le differenze sulla console dall'elemento più esterno a quello più interno.

È possibile aggiungere un po 'di logica all'interno per gestire le matrici. Forse prima ordina gli array. Questa è una soluzione molto flessibile.

MODIFICARE

Modificato da _.merge a _.mergeCon l'aggiornamento del lodash. Grazie Aviron per aver notato il cambiamento.


6
In lodash 4.15.0 _.merge con la funzione di personalizzazione non è più supportato, quindi dovresti usare _.mergeWith.
Aviran Cohen,

1
questa funzione è eccezionale ma non funziona nell'oggetto nidificato.
Joe Allen

14

Ecco una libreria JavaScript che puoi usare per trovare diff tra due oggetti JavaScript:

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

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

È possibile utilizzare la libreria diff ricorsiva nel browser e Node.js. Per il browser, procedi come segue:

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

Considerando che in node.js puoi richiedere il modulo 'recursive-diff' e usarlo come di seguito:

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

Questo non terrà conto delle modifiche alle proprietà della Data, ad esempio.
trollkotze,

viene aggiunto il supporto per la data
Anant,

9

Al giorno d'oggi, ci sono alcuni moduli disponibili per questo. Di recente ho scritto un modulo per farlo, perché non ero soddisfatto dei numerosi moduli differenti che ho trovato. Si chiama odiff: https://github.com/Tixit/odiff . Ho anche elencato un sacco di moduli più popolari e perché non erano accettabili nel readme di odiff, che potresti dare un'occhiata se odiffnon avesse le proprietà che desideri. 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}]}
]
*/

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

Esiste un modulo npm con oltre 500k download settimanali: https://www.npmjs.com/package/deep-object-diff

Mi piace l'oggetto come la rappresentazione delle differenze - soprattutto è facile vedere la struttura, quando è formata.

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

Ho usato questo pezzo di codice per fare il compito 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 oggetto che unirà tutte le modifiche tra il vecchio oggetto e il nuovo oggetto dal tuo modulo


1
Sto usando il framework Ext qui ma è possibile sostituirlo e utilizzare
qualsiasi

L'unione di oggetti è banale e può essere eseguita facilmente come $.extend(true,obj1,obj2)usando jQuery. Questo non è affatto quello di cui ho bisogno. Ho bisogno della differenza tra i due oggetti e non della loro combinazione.
Martin Jespersen,

è bello che Ext sia usato qui
perossido

2

Ho sviluppato la funzione denominata "compareValue ()" in Javascript. restituisce se il valore è uguale o meno. Ho chiamato compareValue () in per il ciclo di un oggetto. 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);


1

So di essere in ritardo alla festa, ma avevo bisogno di qualcosa di simile che le risposte di cui sopra non fossero state d'aiuto.

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à modificata non fosse un campo temporaneo, calcolato. In altre parole, volevo ignorare alcune 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.


Includi anche il codice nella tua risposta, non solo un violino.
xpy,

Sembra che defineProperty risolverebbe questo problema con prestazioni migliori, se ricordo bene funziona fino a IE9.
Peter,

Grazie..!! Il tuo codice funziona in modo incantevole e mi ha salvato la giornata. Ho un oggetto json di 1250 linee e mi dà l'esatta o / p che desidero.
Tejas Mehta,

1

Uso semplicemente ramda, per risolvere lo stesso problema, devo sapere cosa è cambiato nel nuovo oggetto. Quindi ecco 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}]

1

Ecco una versione dattiloscritta del codice @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

Ecco una versione modificata di qualcosa trovato su 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

Ho modificato la risposta di @ sbgoran in modo che l'oggetto diff risultante includesse solo i valori modificati e omettesse valori uguali. Inoltre, mostra sia il valore originale che il valore aggiornato .

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

Ho già scritto una funzione per uno dei miei progetti che confronterà un oggetto come opzioni utente con il suo clone interno. Può anche convalidare 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(/^[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 ))
//);

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

*/

0

La funzione più estesa e semplificata dalla risposta di sbgoran.
Ciò consente una scansione approfondita e trova la similarietà 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';
}


0

Mi sono imbattuto qui nel tentativo di cercare un modo per ottenere la differenza tra due oggetti. Questa è la mia soluzione usando 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

Snippet di codice di seguito:

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

Ho preso la risposta sopra da @sbgoran e l'ho modificata per il mio caso come la domanda necessaria, per trattare gli array come set (cioè l'ordine non è importante per 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

Ecco una soluzione che è:

  • Dattiloscritto (ma facilmente convertibile in Javascript)
  • non ha dipendenze lib
  • generico e non si preoccupa di controllare i tipi di oggetto (a parte il object tipo)
  • supporta proprietà con valore undefined
  • deep of not (impostazione predefinita)

Innanzitutto definiamo l'interfaccia del risultato del confronto:

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

con il caso speciale di cambiamento in cui vogliamo sapere quali sono i valori vecchi e nuovi:

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

Quindi possiamo fornire la difffunzione che è semplicemente due loop (con ricorsività se lo 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';
  }
}

Ad esempio, chiamando:

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

sarà di ritorno:

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

e chiamare lo stesso con il deepterzo parametro restituirà:

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.