두 객체 사이의 일반적인 깊은 차이


222

두 개의 객체가 있습니다 : oldObjnewObj.

의 데이터 oldObj는 양식을 채우는 데 사용되었으며 newObj사용자가이 양식의 데이터를 변경하여 제출 한 결과입니다.

두 물체는 깊습니다. 그들은 객체 또는 객체의 배열 등의 속성을 가지고 있습니다-n 레벨 깊이가 될 수 있으므로 diff 알고리즘은 재귀 적이어야합니다.

지금은 단지에서 (추가와 같이 / 업데이트 / 삭제) 변경이 있었는지 알아낼 필요 oldObjnewObj뿐만 아니라, 최선을 다해 그것을 표현하는 방법.

지금까지 내 생각은 genericDeepDiffBetweenObjects폼에 객체를 반환하는 메서드를 작성하는 {add:{...},upd:{...},del:{...}}것이지만 생각했습니다. 다른 사람이 전에 이것을 필요로 했어야합니다.

그래서 ... 누구든지 이것을 할 라이브러리 또는 코드 조각을 알고 있습니까 (아직 JSON 직렬화 가능한 방식으로) 차이를 나타내는 더 나은 방법이 있습니까?

최신 정보:

와 동일한 객체 구조를 사용 newObj하지만 모든 속성 값을 양식의 객체로 변환 하여 업데이트 된 데이터를 나타내는 더 좋은 방법을 생각했습니다 .

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

경우에 따라서 newObj.prop1 = 'new value'그리고 oldObj.prop1 = 'old value'그것을 설정합니다returnObj.prop1 = {type: 'update', data: 'new value'}

업데이트 2 :

그것은 배열이 있기 때문에 우리가 배열 인 속성에 도착하면 진정으로 털이 얻을 [1,2,3]동일로 간주되어야한다 [2,3,1]문자열, INT 및 부울과 같은 값 기반 형의 배열에 대한 간단한 충분히 인,하지만 올 때 처리하기 정말 어려운 도착 객체 및 배열과 같은 참조 유형의 배열.

동일해야하는 배열 예 :

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

이러한 유형의 깊은 가치 평등을 확인하는 것뿐만 아니라 변경 사항을 나타내는 좋은 방법을 찾는 것도 매우 복잡합니다.



2
@ a'r : stackoverflow.com/questions/1200562/ 의 복제본이 아닙니다 ...- 객체를 통과하는 방법을 알고 있습니다. 사소하지 않고 구현하는 데 시간이 오래 걸리기 때문에 선행 기술을 찾고 있습니다. 오히려 처음부터 라이브러리보다 라이브러리를 사용하십시오.
Martin Jespersen

1
실제로 객체의 차이가 필요합니까, 양식 제출 응답의 서버에서 newObj가 생성 되었습니까? 객체의 "서버 업데이트"가없는 경우 적절한 이벤트 리스너를 연결하여 문제를 단순화 할 수 있고 사용자 상호 작용 (객체 변경)시 원하는 변경 목록을 업데이트 / 생성 할 수 있습니다.
sbgoran

1
@sbgoran : newObjDOM의 폼에서 값을 읽는 js 코드에 의해 생성됩니다. 상태를 유지하고 훨씬 쉽게하는 방법에는 여러 가지가 있지만, 연습으로 상태를 유지하고 싶습니다. 또한 다른 사람이 실제로 어떤 사람을 가지고 있는지에 대해 다른 사람들이 어떻게 대처했는지 알기 위해 선행 기술을 찾고 있습니다.
Martin Jespersen

3
다음은 Javascript 객체 쌍 github.com/benjamine/jsondiffpatch 를 diff / patch하는 매우 정교한 라이브러리입니다. 여기에서 볼 수 있습니다 : benjamine.github.io/jsondiffpatch/demo/index.html (면책 조항 : 저자입니다)
Benja

답변:


142

나는 당신이 원하는 것을하고있는 작은 수업을 썼으며 여기서 테스트 할 수 있습니다 .

귀하의 제안과 다른 점은 [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]동일한 것으로 간주하지 않는다는 것입니다. 요소의 순서가 같지 않으면 배열이 같지 않다고 생각하기 때문입니다. 물론 필요한 경우 변경할 수 있습니다. 또한이 코드는 전달 된 기본 값 (이 작업은 "compareValues"메소드에 의해 수행됨)에 따라 임의의 방식으로 diff 오브젝트를 형식화하는 데 사용되는 인수로 기능을 수행하도록 추가로 향상 될 수 있습니다.

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 나쁜 코드는 아닙니다. 그러나 버그가 있습니다 (이 예제를 확인하십시오 : jsfiddle.net/kySNu/3 c 는 생성 undefined되었지만 문자열이어야 함 'i am created'). 또한 깊은 배열 값 비교가 부족하기 때문에 필요한 것을 수행하지 않습니다. 가장 중요하고 복잡한 부분입니다. 참고로 'array' != typeof(obj)배열은 배열의 인스턴스 인 객체이므로 구문 은 쓸모가 없습니다.
마틴 Jespersen

1
코드를 업데이트했지만 결과 객체에서 원하는 값을 확실하지 않습니다. 현재 코드는 첫 번째 객체에서 값을 반환하고 존재하지 않으면 두 번째 객체의 값이 데이터로 설정됩니다.
sbgoran

1
그리고 해당 {type: ..., data:..}객체에 대해 각 인덱스에 대해 얻을 수있는 배열에 대해 "심층 배열 값 비교 부족"을 의미 합니다. 누락 된 것은 두 번째로 첫 번째 배열에서 값을 검색하는 것이지만 내 대답에서 언급했듯이 값의 순서가 같지 않으면 ( [1, 2, 3] is not equal to [3, 2, 1]내 의견으로는) 배열이 동일하다고 생각하지 않습니다 .
sbgoran

6
@MartinJespersen OK, 일반적 으로이 배열을 어떻게 처리합니까 [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]? 이제 첫 번째 배열의 첫 번째 개체가 "value1"또는 "value2"로 업데이트되었습니다. 그리고 이것은 간단한 예입니다. 깊은 중첩으로 인해 훨씬 ​​복잡해질 수 있습니다. 키 위치에 관계없이 깊은 중첩 비교를 원하거나 필요로하는 경우 객체 배열을 만들지 않으면 이전 예와 같이 중첩 객체로 객체를 만듭니다 {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran

2
마지막 견해에 동의합니다. 원래 데이터 구조는 실제 차이점을 찾기 쉬운 것으로 변경해야합니다. 축하합니다, 당신은 그것을 못 박았다 :)
Martin Jespersen

88

밑줄을 사용하여 간단한 차이점 :

var o1 = {a: 1, b: 2, c: 2},
    o2 = {a: 2, b: 1, c: 2};

_.omit(o1, function(v,k) { return o2[k] === v; })

o1해당 부분 이 일치하지만 다음과 같은 값이 다릅니다 o2.

{a: 1, b: 2}

깊은 차이가 있으면 다릅니다.

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

주석에서 @Juhana가 지적한 것처럼 위의 내용은 diff a-> b이며 되돌릴 수 없습니다 (b의 추가 속성은 무시 됨). 대신 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());

전체 example + tests + mixins에 대해서는 http://jsfiddle.net/drzaus/9g5qoxwj/ 를 참조하십시오


왜 다운 보트를 받았는지 확실하지 않은 경우 얕고 간단한 예를 제공하고보다 복잡한 심층 기능을 제공하면 충분합니다.
Seiyria

2
@Seiyria 증오가 싫어하는 것 같아요. 나는 원래 omit깊은 diff 라고 생각했기 때문에 두 가지를 모두 수행 했지만 잘못되었으므로 비교를 위해 포함되었습니다.
drzaus

1
좋은 해결책. 나는 변경 제안 r[k] = ... : vr[k] = ... : {'a':v, 'b':b[k] }, 당신은 두 개의 값을 볼 수있는이 방법.
guyaloni

2
두 개체 모두 객체가 동일하지만 두 번째 요소에 더 많은 요소 (예 : {a:1, b:2}및) 가있는 경우 위 음수를 반환합니다 {a:1, b:2, c:3}.
JJJ

1
_.omitBy대신 이어야합니다 _.omit.
JP

48

ES6 솔루션을 제공하고 싶습니다 ... 단방향 diff o2입니다 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
좋은 해결책이지만 그 if(o1[key] === o1[key])라인 친구 를 확인하고 싶을 수도 있습니다
bm_i

코드가 완성 되었습니까? 나는오고있다Uncaught SyntaxError: Unexpected token ...
Seano

2
나는 솔루션을 좋아하지만 하나의 문제가 있습니다. 객체가 한 수준보다 깊다면 변경된 중첩 객체의 모든 값을 반환하거나 적어도 저에게 일어나는 일입니다.
스퓨리어스

3
네, 이것은 재귀 적이 지 않습니다. @Spurious
Nemesarial

2
이 솔루션을 사용하면 객체의 각 요소에 대해 기존의 모든 요소가 복사되어 배열에 하나의 항목을 추가하기 위해 완전히 새로운 객체가 생성됩니다. 작은 물체의 경우 괜찮지 만 큰 물체의 경우 기하 급수적으로 느려집니다.
Malvineous

22

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

키 / 객체 / 소스를 사용하지 않지만 액세스 해야하는 경우 키를 남겨 두었습니다. 객체 비교는 콘솔이 가장 바깥 쪽 요소에서 가장 안쪽 요소까지 콘솔의 차이점을 인쇄하지 못하게합니다.

배열을 처리하기 위해 내부에 논리를 추가 할 수 있습니다. 아마도 배열을 먼저 정렬하십시오. 이것은 매우 유연한 솔루션입니다.

편집하다

lodash 업데이트로 인해 _.merge에서 _.mergeWith로 변경되었습니다. 변경 사항을 확인한 Aviron에게 감사합니다.


6
lodash 4.15.0에서는 사용자 정의 기능이있는 _.merge가 더 이상 지원되지 않으므로 _.mergeWith를 대신 사용해야합니다.
Aviran Cohen

1
이 기능은 훌륭하지만 중첩 객체에서는 작동하지 않습니다.
Joe Allen

13

다음은 두 JavaScript 객체 사이의 차이점을 찾는 데 사용할 수있는 JavaScript 라이브러리입니다.

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

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

브라우저와 Node.js에서 재귀 -diff 라이브러리를 사용할 수 있습니다. 브라우저의 경우 다음을 수행하십시오.

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

node.js에서 'recursive-diff'모듈이 필요하고 아래처럼 사용할 수 있습니다.

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

예를 들어 날짜 속성의 변경 사항은 설명하지 않습니다.
trollkotze

날짜 지원이 추가되었습니다
Anant

9

요즘에는 사용할 수있는 모듈이 꽤 있습니다. 내가 찾은 수많은 diffing 모듈에 만족하지 않았기 때문에 최근 에이 작업을 수행하기 위해 모듈을 작성했습니다. 해당 호출 odiff: https://github.com/Tixit/odiff . 또한 가장 인기있는 모듈과 readme에서 허용되지 않는 이유를 나열했습니다 . 원하는 속성이없는 odiff경우 살펴볼 수 있습니다 odiff. 예를 들면 다음과 같습니다.

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

매주 5 만 회 이상 다운로드되는 npm 모듈이 있습니다 : https://www.npmjs.com/package/deep-object-diff

차이점의 표현과 같은 객체를 좋아합니다. 특히 형식이 지정되면 구조를 쉽게 볼 수 있습니다.

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

설명하는 작업을 수행하기 위해이 코드를 사용했습니다.

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

그러면 기존 객체와 새 객체 간의 모든 변경 사항을 양식에서 병합하는 새 객체가 생성됩니다.


1
여기 Ext 프레임 워크를 사용하고 있지만이를 대체하고 원하는 다른 프레임 워크를 사용할 수 있습니다.
AMember

객체 병합은 쉽지 않으며 $.extend(true,obj1,obj2)jQuery 를 사용하는 것처럼 쉽게 수행 할 수 있습니다 . 이것은 내가 필요한 것이 아닙니다. 두 개체의 조합이 아닌 두 개체의 차이점이 필요합니다.
Martin Jespersen

Ext가 여기에서 사용되는 것
peroxide

2

Javascript에서 "compareValue ()"라는 함수를 개발했습니다. 값이 같은지 여부를 반환합니다. 한 Object의 for 루프에서 compareValue ()를 호출했습니다. 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

나는 파티에 늦었다는 것을 알고 있지만 위의 답변이 도움이되지 않은 비슷한 것이 필요했습니다.

변수의 변화를 감지하기 위해 Angular의 $ watch 함수를 사용하고있었습니다. 변수에서 속성이 변경되었는지 여부를 알아야 할뿐만 아니라 변경된 속성이 계산 된 임시 필드가 아닌지 확인하고 싶었습니다. 즉, 특정 속성을 무시하고 싶었습니다.

코드는 다음과 같습니다. https://jsfiddle.net/rv01x6jo/

사용 방법은 다음과 같습니다.

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

이것이 누군가를 돕기를 바랍니다.


바이올린뿐만 아니라 답변에 코드를 포함하십시오.
xpy

defineProperty가 더 나은 성능 으로이 문제를 해결하는 것처럼 보입니다. 정확히 기억한다면 IE9까지 작동합니다.
피터

감사..!! 귀하의 코드는 매력처럼 작동하고 하루를 저축했습니다. 나는 1250 줄의 json 객체를 가지고 있으며 내가 원하는 정확한 o / p를 제공합니다.
Tejas Mehta

1

나는 같은 문제를 해결하기 위해 람다를 사용한다. 새로운 객체에서 무엇이 바뀌 었는지 알아야한다. 여기 내 디자인이 있습니다.

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)

결과는 속성 이름과 상태입니다.

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

1

@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

다음은 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

결과 diff 객체 에 변경된 값 만 포함 하고 동일한 값을 생략 하도록 @sbgoran의 답변을 수정했습니다 . 또한 원래 값과 업데이트 된 값을 모두 표시 합니다 .

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

이미 내 프로젝트 중 하나에 대한 기능을 작성하여 객체를 사용자 옵션으로 내부 복제본과 비교합니다. 또한 사용자가 순수한 자바 스크립트로 잘못된 유형의 데이터를 입력하거나 제거한 경우 기본값을 확인하고 기본값으로 바꿀 수도 있습니다.

IE8에서는 100 % 작동합니다. 성공적으로 테스트되었습니다.

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

/ * 결과

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

sbgoran의 답변에서보다 확장되고 단순화 된 기능.
이를 통해 심층 스캔이 가능하고 어레이의 유사성을 찾을 수 있습니다.

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

나는 두 물체의 차이를 얻는 방법을 찾으려고 여기에서 우연히 발견했다. 이것은 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

아래 코드 스 니펫 :

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

위의 답변을 @sbgoran으로 가져 와서 배열을 세트로 취급하기 위해 필요한 질문과 동일하게 내 경우에 맞게 수정했습니다 (즉, 순서는 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

해결책은 다음과 같습니다.

  • 타이프 스크립트 (하지만 자바 스크립트로 쉽게 변환 가능)
  • lib 의존성이 없다
  • 일반적이며 객체 유형 확인에 신경 쓰지 않습니다 (유형 제외 object)
  • 가치있는 속성을 지원합니다 undefined
  • 깊이 없음 (기본값)

먼저 비교 결과 인터페이스를 정의합니다.

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

과거와 새로운 가치가 무엇인지 알고 싶은 특별한 변화의 경우 :

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

그럼 우리가 제공 할 수 있습니다 diff(경우 recursivity으로 단지 두 개의 루프입니다 기능 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';
  }
}

예를 들어 다음을 호출합니다.

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

돌아올 것이다 :

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

deep세 번째 매개 변수로 동일하게 호출하면 다음이 반환됩니다.

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.