Diferencia profunda genérica entre dos objetos

222

Tengo dos objetos: oldObjy newObj.

Los datos oldObjse utilizaron para completar un formulario y newObjes el resultado de que el usuario haya cambiado los datos en este formulario y los haya enviado.

Ambos objetos son profundos, es decir. tienen propiedades que son objetos o matrices de objetos, etc., pueden tener n niveles de profundidad, por lo tanto, el algoritmo diff debe ser recursivo.

Ahora necesito no solo averiguar qué se cambió (como agregado / actualizado / eliminado) de oldObja newObj, sino también cómo representarlo mejor.

Hasta ahora, mis pensamientos eran simplemente construir un genericDeepDiffBetweenObjectsmétodo que devolviera un objeto en el formulario, {add:{...},upd:{...},del:{...}}pero luego pensé: alguien más debe haber necesitado esto antes.

Entonces ... ¿alguien sabe de una biblioteca o un fragmento de código que haga esto y tal vez tenga una forma aún mejor de representar la diferencia (de una manera que todavía es serializable JSON)?

Actualizar:

He pensado en una mejor manera de representar los datos actualizados, utilizando la misma estructura de objeto newObj, pero convirtiendo todos los valores de propiedad en objetos en el formulario:

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

Entonces, newObj.prop1 = 'new value'y oldObj.prop1 = 'old value'estableceríareturnObj.prop1 = {type: 'update', data: 'new value'}

Actualización 2:

Se vuelve realmente difícil cuando llegamos a las propiedades que son matrices, ya que la matriz [1,2,3]debe contarse como igual a [2,3,1], que es lo suficientemente simple para las matrices de tipos basados ​​en valores como string, int & bool, pero se vuelve realmente difícil de manejar cuando se trata de matrices de tipos de referencia como objetos y matrices.

Ejemplos de matrices que deberían ser iguales:

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

No solo es bastante complejo verificar este tipo de igualdad de valores profundos, sino también encontrar una buena manera de representar los cambios que podrían ocurrir.

Martin Jespersen
fuente
2
@ a'r: No es un duplicado de stackoverflow.com/questions/1200562/… - Sé cómo atravesar los objetos, estoy buscando la técnica anterior ya que esto no es trivial y tomará tiempo real para implementar, y yo preferiría usar una biblioteca que hacerlo desde cero.
Martin Jespersen
1
¿Realmente necesita una diferencia de objetos? ¿Es ese nuevoObj generado desde el servidor en la respuesta de envío del formulario? Porque si no tiene "actualizaciones de servidor" de un objeto, podría simplificar su problema adjuntando escuchas de eventos apropiados y, tras la interacción del usuario (cambio de objeto), podría actualizar / generar la lista de cambios deseados.
sbgoran
1
@sbgoran: newObjes generado por el código js que lee valores de un formulario en el DOM. Hay varias formas de mantener el estado y hacerlo mucho más fácil, pero me gustaría mantenerlo sin estado como ejercicio. También estoy buscando arte previo para ver cómo otros podrían haber abordado esto, si es que alguien lo ha hecho.
Martin Jespersen
3
aquí hay una biblioteca muy sofisticada para diferenciar / parchear cualquier par de objetos Javascript github.com/benjamine/jsondiffpatch , puede verlo en vivo aquí: benjamine.github.io/jsondiffpatch/demo/index.html (descargo de responsabilidad: soy el autor)
Benja

Respuestas:

141

Escribí una pequeña clase que está haciendo lo que quieres, puedes probarla aquí .

Lo único que es diferente de su propuesta es que no considero [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]que sea igual, porque creo que las matrices no son iguales si el orden de sus elementos no es el mismo. Por supuesto, esto se puede cambiar si es necesario. Además, este código se puede mejorar aún más para tomar la función como argumento que se utilizará para formatear el objeto diff de manera arbitraria en función de los valores primitivos pasados ​​(ahora este trabajo se realiza mediante el 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 (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);

sbgoran
fuente
3
+1 No es un mal fragmento de código. Hay un error sin embargo (comprobar este ejemplo Salida: jsfiddle.net/kySNu/3 c se crea como undefined, pero debe ser la cadena 'i am created'), y además no hace lo que necesito ya que le falta el valor de matriz profunda comparar que es el parte más crucial (y compleja / difícil). Como nota al margen, la construcción 'array' != typeof(obj)es inútil ya que las matrices son objetos que son instancias de matrices.
Martin Jespersen
1
Actualicé el código, pero no estoy seguro de qué valor desea en el objeto resultante, en este momento el código está devolviendo el valor del primer objeto y, si no existe, el valor del segundo se establecerá como datos.
sbgoran
1
¿Y qué quiere decir con "falta de comparación del valor de matriz profunda" para las matrices que obtendrá para cada índice de ese {type: ..., data:..}objeto? Lo que falta es buscar el valor de la primera matriz en la segunda, pero como mencioné en mi respuesta, no creo que las matrices sean iguales si el orden de sus valores no es igual ( [1, 2, 3] is not equal to [3, 2, 1]en mi opinión).
sbgoran
66
@MartinJespersen OK, ¿cómo tratar de forma genérica este matrices a continuación: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Ahora es el primer objeto en la primera matriz actualizado con "valor1" o "valor2". Y este es un ejemplo simple, podría complicarse mucho con el anidamiento profundo. Si usted quiere / necesita la comparación de profundidad de anidamiento independientemente de la posición clave no crean matrices de objetos, crear objetos con objetos anidados, como por ejemplo anterior: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran
2
Estoy de acuerdo con su último punto de vista: la estructura de datos original debe cambiarse a algo que sea más fácil de hacer una diferencia real. Felicidades, lo has clavado :)
Martin Jespersen
88

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

Resultados en las partes de o1que corresponden pero con diferentes valores en o2:

{a: 1, b: 2}

Sería diferente para una diferencia profunda:

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 señaló @Juhana en los comentarios, lo anterior es solo una diferencia a -> b y no reversible (lo que significa que las propiedades adicionales en b serían ignoradas). Utilice en su lugar 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());

Ver http://jsfiddle.net/drzaus/9g5qoxwj/ para ver ejemplos completos + pruebas + mixins

drzaus
fuente
No estoy seguro de por qué lo votaron negativamente, esto fue suficiente ya que proporcionó un ejemplo simple y superficial, así como una función profunda más compleja.
Seiyria
2
@Seiyria odiará odiar, supongo ... Hice ambas cosas porque originalmente pensé omitque sería una gran diferencia, pero estaba equivocado, así que también lo incluí para comparar.
drzaus
1
Buena solución Yo sugeriría cambiar r[k] = ... : ven r[k] = ... : {'a':v, 'b':b[k] }, de esta manera se pueden ver dos valores.
guyaloni
2
Ambos devuelven un falso negativo cuando los objetos son idénticos pero el segundo tiene más elementos, por ejemplo, {a:1, b:2}y {a:1, b:2, c:3}.
JJJ
1
Debería ser en _.omitBylugar de _.omit.
JP
48

Me gustaría ofrecer una solución ES6 ... Esta es una diferencia unidireccional, lo que significa que devolverá claves / valores o2que no son idénticos a sus contrapartes en 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]
  }
}, {})
senornestor
fuente
3
Buena solución, pero es posible que desee comprobar esa if(o1[key] === o1[key])línea amigo
bm_i
¿Está completo el código? Me estoy poniendoUncaught SyntaxError: Unexpected token ...
Seano
2
Me gusta la solución, pero tiene un problema: si el objeto es más profundo que un nivel, devolverá todos los valores en los objetos anidados modificados, o al menos eso es lo que me está sucediendo.
Espurio
3
Sí, esto no es recursivo @Spurious
Nemesarial
2
Solo tenga en cuenta que con esta solución, para cada elemento en el objeto, obtendrá un objeto completamente nuevo construido con todos los elementos existentes copiados en él solo para agregar un elemento a la matriz. Para objetos pequeños está bien, pero se ralentizará exponencialmente para objetos más grandes.
Malvineous
22

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

No uso clave / objeto / fuente, pero lo dejé allí si necesita acceder a ellos. La comparación de objetos simplemente evita que la consola imprima las diferencias en la consola desde el elemento más externo hasta el elemento más interno.

Puede agregar algo de lógica dentro para manejar matrices. Quizás clasifique las matrices primero. Esta es una solución muy flexible.

EDITAR

Cambió de _.merge a _.mergeWith debido a la actualización lodash. Gracias Aviron por notar el cambio.

toshiomagic
fuente
66
En lodash 4.15.0 _.merge con la función de personalización ya no es compatible, por lo que debe usar _.mergeWith en su lugar.
Aviran Cohen
1
Esta función es excelente pero no funciona en objetos anidados.
Joe Allen
13

Aquí hay una biblioteca de JavaScript que puede usar para encontrar diferencias entre dos objetos de JavaScript:

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

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

Puede usar la biblioteca recursive-diff en el navegador, así como Node.js. Para el navegador, haga lo siguiente:

<script type="text" src="https://unpkg.com/[email protected]/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>

Mientras que en node.js puede requerir el módulo 'recursive-diff' y usarlo como a continuación:

const diff = require('recursive-diff');
const ob1 = {a: 1}, ob2: {b:2};
const diff = diff.getDiff(ob1, ob2);
Una hormiga
fuente
Esto no tendrá en cuenta los cambios en las propiedades de fecha, por ejemplo.
trollkotze
fecha se agrega soporte
Anant
9

En estos días, hay bastantes módulos disponibles para esto. Recientemente escribí un módulo para hacer esto, porque no estaba satisfecho con los numerosos módulos diferentes que encontré. Se llama odiff: https://github.com/Tixit/odiff . También enumeré un montón de los módulos más populares y por qué no eran aceptables en el archivo Léame odiff, que podría consultar si odiffno tiene las propiedades que desea. Aquí hay un ejemplo:

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}]}
]
*/
BT
fuente
7
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

Hay un módulo npm con más de 500k descargas semanales: https://www.npmjs.com/package/deep-object-diff

Me gusta el objeto como representación de las diferencias, especialmente es fácil ver la estructura, cuando está formateada.

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'
}
*/
Felix Furtmayr
fuente
2

He usado este código para realizar la tarea que usted describe:

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

esto le dará un nuevo objeto que combinará todos los cambios entre el objeto antiguo y el nuevo objeto de su formulario

Un miembro
fuente
1
Estoy usando el marco Ext aquí, pero puede reemplazarlo y usar cualquier otro marco que desee ...
AMember
Fusionar objetos es trivial y se puede hacer tan fácil como $.extend(true,obj1,obj2)usar jQuery. Esto no es para nada lo que necesito. Necesito la diferencia entre los dos objetos, no la combinación de ellos.
Martin Jespersen
es genial que Ext se use aquí
peróxido
2

Desarrollé la función llamada "compareValue ()" en Javascript. devuelve si el valor es igual o no. He llamado a compareValue () en el bucle for de un Object. Puede obtener la diferencia de dos objetos en 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);

jarangseo
fuente
1

Sé que llego tarde a la fiesta, pero necesitaba algo similar que las respuestas anteriores no ayudaron.

Estaba usando la función $ watch de Angular para detectar cambios en una variable. No solo necesitaba saber si una propiedad había cambiado en la variable, sino que también quería asegurarme de que la propiedad que cambió no era un campo calculado temporal. En otras palabras, quería ignorar ciertas propiedades.

Aquí está el código: https://jsfiddle.net/rv01x6jo/

Aquí se explica cómo 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]);

Espero que esto ayude a alguien.

a11smiles
fuente
Incluya también el código en su respuesta, no solo un violín.
xpy
Parece que defineProperty resolvería este problema con un mejor rendimiento, si no recuerdo mal, funciona hasta IE9.
Peter
Gracias..!! Tu código funciona a las mil maravillas y me salvó el día. Tengo un objeto json de 1250 líneas y me da la o / p exacta que quiero.
Tejas Mehta
1

Solo uso ramda, para resolver el mismo problema, necesito saber qué cambia en el nuevo objeto. Entonces aquí mi diseño.

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)

El resultado es el nombre de la propiedad y su estado.

[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]
Ivan Titkov
fuente
1

Aquí hay una versión mecanografiada del código @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;

  }
}
wzr1337
fuente
1

Aquí hay una versión modificada de algo encontrado en 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;
        }
    });
}
Nico
fuente
1

Modifiqué la respuesta de @ sbgoran para que el objeto diff resultante incluya solo los valores modificados y omita los valores que eran iguales. Además, muestra tanto el valor original como el valor actualizado .

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

Ya escribí una función para uno de mis proyectos que comparará un objeto como opciones de usuario con su clon interno. También puede validar e incluso reemplazar por valores predeterminados si el usuario ingresó un tipo de datos incorrecto o lo eliminó, en javascript puro.

En IE8 100% funciona. Probado con éxito.

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

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

*/
Samad Aghaei
fuente
0

La función más extendida y simplificada de la respuesta de sbgoran.
Esto permite un escaneo profundo y encuentra la similitud de una 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';
}

StefansArya
fuente
0

Me tropecé aquí tratando de buscar una manera de obtener la diferencia entre dos objetos. Esta es mi solución 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

Fragmento de código a continuación:

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>

Jee Mok
fuente
0

Tomé la respuesta anterior de @sbgoran y la modifiqué para mi caso igual que la pregunta necesaria, para tratar las matrices como conjuntos (es decir, el orden no es importante para 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);
  }
 };
}();
Bashar Ali Labadi
fuente
0

Aquí hay una solución que es:

  • Mecanografiado (pero fácilmente convertible a Javascript)
  • no tener dependencias lib
  • genérico, y no le importa verificar los tipos de objetos (aparte del objecttipo)
  • admite propiedades con valor undefined
  • profundidad de no (predeterminado)

Primero definimos la interfaz de resultados de comparación:

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

con el caso especial de cambio donde queremos saber cuáles son los valores antiguos y nuevos:

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

Entonces podemos proporcionar la difffunción que es simplemente dos bucles (con recursividad si deepes 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';
  }
}

Como ejemplo, llamando a:

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

volverá:

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

y llamando al mismo con el deeptercer parámetro devolverá:

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