Diferencia profunda genérica entre dos objetos.

Resuelto Martin Jespersen asked hace 13 años • 28 respuestas

Tengo dos objetos: oldObjy newObj.

Los datos oldObjse utilizaron para completar un formulario y newObjson el resultado de que el usuario cambie los datos en este formulario y los envíe.

Ambos objetos son profundos, es decir. tienen propiedades que son objetos o conjuntos de objetos, etc.; pueden tener n niveles de profundidad, por lo que el algoritmo de diferenciación debe ser recursivo.

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

Hasta ahora, mi idea era simplemente crear 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 conoce una biblioteca o un fragmento de código que haga esto y tal vez tenga una manera aún mejor de representar la diferencia (de una manera que aún sea serializable en JSON)?

Actualizar:

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

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

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

Actualización 2:

Se vuelve realmente complicado cuando llegamos a propiedades que son matrices, ya que la matriz [1,2,3]debe contarse como igual a [2,3,1], lo cual es bastante simple para matrices de tipos basados ​​en valores como string, int y 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 sólo es bastante complejo comprobar este tipo de igualdad profunda de valores, sino también encontrar una buena manera de representar los cambios que podrían producirse.

Martin Jespersen avatar Dec 20 '11 15:12 Martin Jespersen
Aceptado

Escribí una pequeña clase que hace lo que quieres, puedes probarla aquí .

Lo único que difiere de tu propuesta es que no considero

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

y

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

ser 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 que tome la función como argumento que se usará para formatear el objeto diff de forma 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);
Expandir fragmento

sbgoran avatar Dec 21 '2011 21:12 sbgoran

Usando subrayado, una diferencia simple:

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 o1que corresponden pero con valores diferentes 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 es reversible (lo que significa que las propiedades adicionales en b se ignorarían). 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());

Consulte http://jsfiddle.net/drzaus/9g5qoxwj/ para ver un ejemplo completo+pruebas+mixins

drzaus avatar Sep 03 '2014 18:09 drzaus