Diferença profunda genérica entre dois objetos

222

Eu tenho dois objetos: oldObje newObj.

Os dados oldObjforam usados ​​para preencher um formulário e newObjsão o resultado do usuário alterando os dados neste formulário e enviando-os.

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

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

Até agora, meus pensamentos eram apenas construir um genericDeepDiffBetweenObjectsmétodo que retornasse um objeto no formulário, {add:{...},upd:{...},del:{...}}mas então pensei: alguém já deveria ter precisado disso antes.

Então ... alguém sabe de uma biblioteca ou de um código que fará isso e talvez tenha uma maneira ainda melhor de representar a diferença (de uma maneira que ainda seja serializável em JSON)?

Atualizar:

Eu pensei em uma maneira melhor de representar os dados atualizados, usando a mesma estrutura de objeto que newObj, mas transformando todos os valores de propriedade em objetos no formulário:

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

Então, se newObj.prop1 = 'new value'e oldObj.prop1 = 'old value'definiriareturnObj.prop1 = {type: 'update', data: 'new value'}

Atualização 2:

Fica realmente complicado quando chegamos a propriedades que são matrizes, já que a matriz [1,2,3]deve ser contada como igual a [2,3,1], o que é simples o suficiente para matrizes de tipos baseados em valor, como string, int e bool, mas fica realmente difícil de lidar quando se trata de matrizes de tipos de referência, como objetos e matrizes.

Matrizes de exemplo que devem ser encontradas iguais:

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

Não é apenas complexo verificar esse tipo de profunda igualdade de valores, mas também descobrir uma boa maneira de representar as mudanças que possam ser.

Martin Jespersen
fonte
1
possível duplicação da diferença nos objetos JSON usando Javascript / JQuery
20/12/11
2
@ a'r: Não é uma duplicata do stackoverflow.com/questions/1200562/… - Eu sei como atravessar os objetos, estou procurando pela arte anterior, pois isso não é trivial e levará tempo real para ser implementado. prefiro usar uma biblioteca do que fazê-la do zero.
Martin Jespersen
1
Você realmente precisa de diff de objetos, esse newObj gerado a partir do servidor no formulário envia resposta? Como se você não tiver "atualizações do servidor" de um objeto, poderá simplificar seu problema anexando ouvintes de eventos apropriados e, após a interação do usuário (alteração de objeto), poderá atualizar / gerar a lista de alterações desejadas.
precisa saber é o seguinte
1
@sbgoran: newObjé gerado pelos valores de leitura de código js de um formulário no DOM. Existem várias maneiras de manter o estado e fazer isso com muito mais facilidade, mas eu gostaria de mantê-lo apátrida como exercício. Também estou procurando arte anterior para ver como os outros podem ter enfrentado isso, se é que alguém já o fez.
Martin Jespersen
3
aqui está uma biblioteca muito sofisticada para diferenciar / corrigir qualquer par de objetos Javascript github.com/benjamine/jsondiffpatch, você pode vê-la ao vivo aqui: benjamine.github.io/jsondiffpatch/demo/index.html (isenção de responsabilidade: sou o autor)
Benja

Respostas:

141

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

A única coisa diferente da sua proposta é que eu não considero [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]a mesma coisa, porque acho que as matrizes não são iguais se a ordem de seus elementos não for a mesma. Claro que isso pode ser alterado, se necessário. Além disso, esse código pode ser aprimorado ainda mais para que funcione como argumento que será usado para formatar objetos diff de maneira arbitrária, com base nos valores primitivos passados ​​(agora esse trabalho é feito pelo método "compareValues").

var deepDiffMapper = function () {
  return {
    VALUE_CREATED: 'created',
    VALUE_UPDATED: 'updated',
    VALUE_DELETED: 'deleted',
    VALUE_UNCHANGED: 'unchanged',
    map: function(obj1, obj2) {
      if (this.isFunction(obj1) || this.isFunction(obj2)) {
        throw 'Invalid argument. Function given, object expected.';
      }
      if (this.isValue(obj1) || this.isValue(obj2)) {
        return {
          type: this.compareValues(obj1, obj2),
          data: obj1 === undefined ? obj2 : obj1
        };
      }

      var diff = {};
      for (var key in obj1) {
        if (this.isFunction(obj1[key])) {
          continue;
        }

        var value2 = undefined;
        if (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
fonte
3
+1 Não é um código ruim. No entanto, há um erro (veja este exemplo: jsfiddle.net/kySNu/3 c é criado como undefineddeve ser a string 'i am created') e, além disso, ele não faz o que eu preciso, pois falta o valor profundo da matriz compare, que é o parte mais crucial (e complexa / difícil). Como observação lateral, a construção 'array' != typeof(obj)é inútil, pois matrizes são objetos que são instâncias de matrizes.
Martin Jespersen
1
Atualizei o código, mas não tenho certeza de qual valor você deseja no objeto resultante. No momento, o código está retornando valor do primeiro objeto e, se não existir, o valor do segundo será definido como dados.
precisa saber é o seguinte
1
E como você quer dizer "sem a comparação profunda do valor da matriz" para matrizes que você obterá para cada índice desse {type: ..., data:..}objeto. O que está faltando é pesquisar valor da primeira matriz em segundo, mas, como mencionei na minha resposta, não acho que as matrizes sejam iguais se a ordem de seus valores não for igual ( [1, 2, 3] is not equal to [3, 2, 1]na minha opinião).
precisa saber é o seguinte
6
@MartinJespersen OK, como você iria tratar genericamente este matrizes então: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Agora é o primeiro objeto na primeira matriz atualizado com "valor1" ou "valor2". E este é um exemplo simples, pode ficar muito complicado com o aninhamento profundo. Se você quiser / precisar comparação profunda do assentamento, independentemente da posição da chave não criar matrizes de objetos, criar objetos com objetos aninhados como por exemplo anterior: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran
2
Concordo com você, último ponto de vista - a estrutura de dados original deve ser alterada para algo mais fácil de fazer uma comparação real. Parabéns, você acertou em cheio :)
Martin Jespersen
88

Usando Underscore, uma simples comparação:

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 nas partes o1correspondentes, mas com valores diferentes em o2:

{a: 1, b: 2}

Seria diferente para uma diferença 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;
}

Conforme apontado por @Juhana nos comentários, o acima é apenas um diff a -> be não é reversível (o que significa que propriedades extras em b seriam ignoradas). Use em vez disso a -> b -> a:

(function(_) {
  function deepDiff(a, b, r) {
    _.each(a, function(v, k) {
      // already checked this or equal...
      if (r.hasOwnProperty(k) || b[k] === v) return;
      // but what if it returns an empty object? still attach?
      r[k] = _.isObject(v) ? _.diff(v, b[k]) : v;
    });
  }

  /* the function */
  _.mixin({
    diff: function(a, b) {
      var r = {};
      deepDiff(a, b, r);
      deepDiff(b, a, r);
      return r;
    }
  });
})(_.noConflict());

Veja http://jsfiddle.net/drzaus/9g5qoxwj/ para obter exemplos completos + testes + mixins

drzaus
fonte
Não tendo certeza do motivo pelo qual você recebeu voto negativo, isso foi suficiente, pois você forneceu um exemplo simples e superficial, além de uma função profunda mais complexa.
Seiyria
2
Odiadores da @Seiyria odeiam, eu acho ... fiz os dois porque, originalmente, pensei omitque seria uma dificuldade profunda, mas estava errado, então incluímos também para comparação.
drzaus #
1
Ótima solução. Gostaria de sugerir a mudar r[k] = ... : vem r[k] = ... : {'a':v, 'b':b[k] }, desta forma você pode ver dois valores.
guyaloni
2
Ambos retornam um falso negativo quando os objetos são idênticos, mas o segundo possui mais elementos, por exemplo, {a:1, b:2}e {a:1, b:2, c:3}.
JJJ
1
Deve ser em _.omitByvez de _.omit.
JP
48

Eu gostaria de oferecer uma solução ES6 ... Essa é uma diferença unidirecional, o que significa que ele retornará chaves / valores o2que não são idênticos aos seus equivalentes em 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
fonte
3
Solução agradável, mas você pode querer verificar que if(o1[key] === o1[key])linha de cara
bm_i
O código está completo? Estou recebendo #Uncaught SyntaxError: Unexpected token ...
Seano
2
Gosto da solução, mas ela tem um problema: se o objeto for mais profundo do que um nível, ele retornará todos os valores nos objetos aninhados alterados - ou pelo menos é o que está acontecendo comigo.
Spurious
3
Sim, isso não é recursivo @Spurious
Nemesarial
2
Lembre-se de que, com esta solução, para cada elemento do objeto, você obtém um objeto totalmente novo, construído com todos os elementos existentes copiados, apenas para adicionar um item à matriz. Para objetos pequenos, tudo bem, mas diminuirá exponencialmente para objetos maiores.
Malvineous
22

Usando o Lodash:

_.mergeWith(oldObj, newObj, function (objectValue, sourceValue, key, object, source) {
    if ( !(_.isEqual(objectValue, sourceValue)) && (Object(objectValue) !== objectValue)) {
        console.log(key + "\n    Expected: " + sourceValue + "\n    Actual: " + objectValue);
    }
});

Eu não uso chave / objeto / fonte, mas deixei lá se você precisar acessá-los. A comparação de objetos apenas impede que o console imprima as diferenças no console do elemento mais externo para o elemento mais interno.

Você pode adicionar alguma lógica interna para manipular matrizes. Talvez classifique as matrizes primeiro. Esta é uma solução muito flexível.

EDITAR

Alterado de _.merge para _.mergeWith devido à atualização do lodash. Obrigado Aviron por perceber a mudança.

toshiomagic
fonte
6
No lodash 4.15.0, a função _.merge com personalizador não é mais suportada; portanto, você deve usar _.mergeWith.
Aviran Cohen
1
essa função é ótima, mas não funciona no objeto aninhado.
Joe Allen
13

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

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

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

Você pode usar a biblioteca diff-recursiva no navegador e também no Node.js. Para navegador, faça o seguinte:

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

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

const diff = require('recursive-diff');
const ob1 = {a: 1}, ob2: {b:2};
const diff = diff.getDiff(ob1, ob2);
Uma formiga
fonte
Isso não levará em conta alterações nas propriedades da data, por exemplo.
trollkotze
data em que o suporte foi adicionado
Anant
9

Atualmente, existem alguns módulos disponíveis para isso. Recentemente, escrevi um módulo para fazer isso, porque não estava satisfeito com os vários módulos diferentes que encontrei. É chamado odiff: https://github.com/Tixit/odiff . Também listei vários módulos mais populares e por que eles não eram aceitáveis ​​no leia-me odiff, dos quais você pode dar uma olhada se odiffnão tiver as propriedades que deseja. Aqui está um exemplo:

var a = [{a:1,b:2,c:3},              {x:1,y: 2, z:3},              {w:9,q:8,r:7}]
var b = [{a:1,b:2,c:3},{t:4,y:5,u:6},{x:1,y:'3',z:3},{t:9,y:9,u:9},{w:9,q:8,r:7}]

var diffs = odiff(a,b)

/* diffs now contains:
[{type: 'add', path:[], index: 2, vals: [{t:9,y:9,u:9}]},
 {type: 'set', path:[1,'y'], val: '3'},
 {type: 'add', path:[], index: 1, vals: [{t:4,y:5,u:6}]}
]
*/
BT
fonte
7
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

Existe um módulo npm com mais de 500 mil downloads semanais: https://www.npmjs.com/package/deep-object-diff

Gosto do objeto como representação das diferenças - especialmente é fácil ver a estrutura quando está formatada.

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

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

function mergeRecursive(obj1, obj2) {
    for (var p in obj2) {
        try {
            if(obj2[p].constructor == Object) {
                obj1[p] = mergeRecursive(obj1[p], obj2[p]);
            }
            // Property in destination object set; update its value.
            else if (Ext.isArray(obj2[p])) {
                // obj1[p] = [];
                if (obj2[p].length < 1) {
                    obj1[p] = obj2[p];
                }
                else {
                    obj1[p] = mergeRecursive(obj1[p], obj2[p]);
                }

            }else{
                obj1[p] = obj2[p];
            }
        } catch (e) {
            // Property in destination object not set; create it and set its value.
            obj1[p] = obj2[p];
        }
    }
    return obj1;
}

isso fornecerá um novo objeto que mesclará todas as alterações entre o objeto antigo e o novo objeto do seu formulário

Um membro
fonte
1
Eu estou usando o framework Ext aqui, mas você pode substituí-lo e usar o que sempre outro quadro você gostaria ...
amember
A mesclagem de objetos é trivial e pode ser tão fácil quanto $.extend(true,obj1,obj2)usar o jQuery. Não é disso que eu preciso. Preciso da diferença entre os dois objetos, não da combinação deles.
Martin Jespersen
é ótimo que Ext seja usado aqui
peróxido
2

Eu desenvolvi a função denominada "compareValue ()" em Javascript. retorna se o valor é igual ou não. Eu chamei compareValue () no loop for de um Object. você pode obter a diferença de dois objetos no 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
fonte
1

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

Eu estava usando a função $ watch do Angular para detectar alterações em uma variável. Não só precisava saber se uma propriedade havia mudado na variável, mas também queria ter certeza de que a propriedade que mudou não fosse um campo calculado temporário. Em outras palavras, eu queria ignorar certas propriedades.

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

Veja como usá-lo:

// To only return the difference
var difference = diff(newValue, oldValue);  

// To exclude certain properties
var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);

Espero que isso ajude alguém.

a11smiles
fonte
Inclua também o código na sua resposta, não apenas um violino.
xpy
Parece que defineProperty resolveria esse problema com melhor desempenho, se bem me lembro, ele funciona até o IE9.
Peter
Obrigado..!! Seu código funciona como charme e salvou meu dia. Eu tenho json objeto de 1250 linhas e isso me dá o / p exato que eu quero.
Tejas Mehta
1

Eu apenas uso ramda, para resolver o mesmo problema, eu preciso saber o que é alterado no novo objeto. Então aqui está o meu design.

const oldState = {id:'170',name:'Ivab',secondName:'Ivanov',weight:45};
const newState = {id:'170',name:'Ivanko',secondName:'Ivanov',age:29};

const keysObj1 = R.keys(newState)

const filterFunc = key => {
  const value = R.eqProps(key,oldState,newState)
  return {[key]:value}
}

const result = R.map(filterFunc, keysObj1)

O resultado é, nome da propriedade e seu status.

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

Aqui está uma versão datilografada do 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
fonte
1

Aqui está uma versão modificada de algo encontrado no 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
fonte
1

Modifiquei a resposta de @ sbgoran para que o objeto diff resultante inclua apenas os valores alterados e omita os valores iguais. Além disso, mostra o valor original e o valor atualizado .

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

Eu já escrevi uma função para um dos meus projetos que comparará um objeto como opções de usuário com seu clone interno. Ele também pode validar e até substituir por valores padrão se o usuário digitar dados incorretos ou removidos, em javascript puro.

No IE8, 100% funciona. Testado com sucesso.

//  ObjectKey: ["DataType, DefaultValue"]
reference = { 
    a : ["string", 'Defaul value for "a"'],
    b : ["number", 300],
    c : ["boolean", true],
    d : {
        da : ["boolean", true],
        db : ["string", 'Defaul value for "db"'],
        dc : {
            dca : ["number", 200],
            dcb : ["string", 'Default value for "dcb"'],
            dcc : ["number", 500],
            dcd : ["boolean", true]
      },
      dce : ["string", 'Default value for "dce"'],
    },
    e : ["number", 200],
    f : ["boolean", 0],
    g : ["", 'This is an internal extra parameter']
};

userOptions = { 
    a : 999, //Only string allowed
  //b : ["number", 400], //User missed this parameter
    c: "Hi", //Only lower case or case insitive in quotes true/false allowed.
    d : {
        da : false,
        db : "HelloWorld",
        dc : {
            dca : 10,
            dcb : "My String", //Space is not allowed for ID attr
            dcc: "3thString", //Should not start with numbers
            dcd : false
      },
      dce: "ANOTHER STRING",
    },
    e: 40,
    f: true,
};


function compare(ref, obj) {

    var validation = {
        number: function (defaultValue, userValue) {
          if(/^[0-9]+$/.test(userValue))
            return userValue;
          else return defaultValue;
        },
        string: function (defaultValue, userValue) {
          if(/^[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
fonte
0

A função mais extensa e simplificada da resposta de sbgoran.
Isso permite uma varredura profunda e encontra a similaridade de uma matriz.

var result = objectDifference({
      a:'i am unchanged',
      b:'i am deleted',
      e: {a: 1,b:false, c: null},
      f: [1,{a: 'same',b:[{a:'same'},{d: 'delete'}]}],
      g: new Date('2017.11.25'),
      h: [1,2,3,4,5]
  },
  {
      a:'i am unchanged',
      c:'i am created',
      e: {a: '1', b: '', d:'created'},
      f: [{a: 'same',b:[{a:'same'},{c: 'create'}]},1],
      g: new Date('2017.11.25'),
      h: [4,5,6,7,8]
  });
console.log(result);

function objectDifference(obj1, obj2){
    if((dataType(obj1) !== 'array' && dataType(obj1) !== 'object') || (dataType(obj2) !== 'array' && dataType(obj2) !== 'object')){
        var type = '';

        if(obj1 === obj2 || (dataType(obj1) === 'date' && dataType(obj2) === 'date' && obj1.getTime() === obj2.getTime()))
            type = 'unchanged';
        else if(dataType(obj1) === 'undefined')
            type = 'created';
        if(dataType(obj2) === 'undefined')
            type = 'deleted';
        else if(type === '') type = 'updated';

        return {
            type: type,
            data:(obj1 === undefined) ? obj2 : obj1
        };
    }
  
    if(dataType(obj1) === 'array' && dataType(obj2) === 'array'){
        var diff = [];
        obj1.sort(); obj2.sort();
        for(var i = 0; i < obj2.length; i++){
            var type = obj1.indexOf(obj2[i]) === -1?'created':'unchanged';
            if(type === 'created' && (dataType(obj2[i]) === 'array' || dataType(obj2[i]) === 'object')){
                diff.push(
                    objectDifference(obj1[i], obj2[i])
                );
                continue;
            }
            diff.push({
                type: type,
                data: obj2[i]
            });
        }

        for(var i = 0; i < obj1.length; i++){
            if(obj2.indexOf(obj1[i]) !== -1 || dataType(obj1[i]) === 'array' || dataType(obj1[i]) === 'object')
                continue;
            diff.push({
                type: 'deleted',
                data: obj1[i]
            });
        }
    } else {
        var diff = {};
        var key = Object.keys(obj1);
        for(var i = 0; i < key.length; i++){
            var value2 = undefined;
            if(dataType(obj2[key[i]]) !== 'undefined')
                value2 = obj2[key[i]];

            diff[key[i]] = objectDifference(obj1[key[i]], value2);
        }

        var key = Object.keys(obj2);
        for(var i = 0; i < key.length; i++){
            if(dataType(diff[key[i]]) !== 'undefined')
                continue;

            diff[key[i]] = objectDifference(undefined, obj2[key[i]]);
        }
    }

    return diff;
}

function dataType(data){
    if(data === undefined || data === null) return 'undefined';
    if(data.constructor === String) return 'string';
    if(data.constructor === Array) return 'array';
    if(data.constructor === Object) return 'object';
    if(data.constructor === Number) return 'number';
    if(data.constructor === Boolean) return 'boolean';
    if(data.constructor === Function) return 'function';
    if(data.constructor === Date) return 'date';
    if(data.constructor === RegExp) return 'regex';
    return 'unknown';
}

StefansArya
fonte
0

Tropecei aqui, tentando encontrar uma maneira de obter a diferença entre dois objetos. Esta é a minha solução usando o 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 de código abaixo:

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

Peguei a resposta acima por @sbgoran e a modifiquei para o meu caso da mesma forma que a pergunta necessária, para tratar matrizes como conjuntos (ou seja, a ordem não é 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
fonte
0

Aqui está uma solução que é:

  • Texto datilografado (mas facilmente convertível em Javascript)
  • não tem dependências de lib
  • genérico e não se preocupa em verificar os tipos de objetos (além do objecttipo)
  • suporta propriedades com valor undefined
  • profundidade de não (padrão)

Primeiro, definimos a interface do resultado da comparação:

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

com o caso especial de mudança, onde queremos saber quais são os valores antigos e novos:

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

Então, podemos fornecer a difffunção que é apenas dois loops (com recursividade se deepfor 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 exemplo, chamando:

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

retornará:

{
  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 chamar o mesmo com o deepterceiro parâmetro retornará:

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