2つのオブジェクト間の一般的な深い差分


222

との2つのオブジェクトがoldObjありnewObjます。

のデータは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、boolなどの値ベースの型の配列では十分簡単ですが、オブジェクトや配列などの参照型の配列。

等しいはずの配列の例:

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

このタイプの深い値の等価性をチェックすることは非常に複雑であるだけでなく、変更の可能性を表す良い方法を理解することも必要です。



2
@ a'r:これは、stackoverflow.com / questions / 1200562 /…の複製ではありません。オブジェクトをトラバースする方法を知っています。これは簡単なことではなく、実装にリアルタイムがかかるため、先行技術を探しています。最初から作成するのではなく、ライブラリを使用したい。
Martin Jespersen、2011

1
オブジェクトの差分が本当に必要ですか、フォーム送信応答でサーバーから生成されたnewObjですか?オブジェクトの「サーバー更新」がない場合は、適切なイベントリスナーをアタッチすることで問題を単純化でき、ユーザーの操作(オブジェクトの変更)時に必要な変更リストを更新/生成できるためです。
sbgoran

1
@sbgoran:newObjDOMのフォームから値を読み取るjsコードによって生成されます。状態を保持し、これをはるかに簡単に行う方法はいくつかありますが、演習として状態を保持しないようにしたいと思います。また、実際に誰かがこれに取り組んでいる場合、他の人がこれにどのように取り組んできたかを確認するために、先行技術を探しています。
Martin Jespersen、2011

3
ここでの差分に非常に洗練されたライブラリだ/ Javascriptのオブジェクトの任意のペアにパッチを適用github.com/benjamine/jsondiffpatchあなたはそれがここに住んで見ることができます:benjamine.github.io/jsondiffpatch/demo/index.html(免責事項:私は著者です)
Benja

回答:


141

私はあなたが望むことをしている小さなクラスを書きました、あなたはここでそれをテストすることができます

あなたの提案と異なるの[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]は、要素の順序が同じでない場合、配列は等しくないと思うので、私は同じであるとは思わないということだけです。もちろん、これは必要に応じて変更できます。また、このコードをさらに拡張して、渡されたプリミティブ値に基づいてdiffオブジェクトを任意の方法でフォーマットするために使用される引数として関数を使用できます(現在、このジョブは「compareValues」メソッドによって実行されます)。

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

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

        var value2 = undefined;
        if (obj2[key] !== undefined) {
          value2 = obj2[key];
        }

        diff[key] = this.map(obj1[key], value2);
      }
      for (var key in obj2) {
        if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
          continue;
        }

        diff[key] = this.map(undefined, obj2[key]);
      }

      return diff;

    },
    compareValues: function (value1, value2) {
      if (value1 === value2) {
        return this.VALUE_UNCHANGED;
      }
      if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
        return this.VALUE_UNCHANGED;
      }
      if (value1 === undefined) {
        return this.VALUE_CREATED;
      }
      if (value2 === undefined) {
        return this.VALUE_DELETED;
      }
      return this.VALUE_UPDATED;
    },
    isFunction: function (x) {
      return Object.prototype.toString.call(x) === '[object Function]';
    },
    isArray: function (x) {
      return Object.prototype.toString.call(x) === '[object Array]';
    },
    isDate: function (x) {
      return Object.prototype.toString.call(x) === '[object Date]';
    },
    isObject: function (x) {
      return Object.prototype.toString.call(x) === '[object Object]';
    },
    isValue: function (x) {
      return !this.isObject(x) && !this.isArray(x);
    }
  }
}();


var result = deepDiffMapper.map({
  a: 'i am unchanged',
  b: 'i am deleted',
  e: {
    a: 1,
    b: false,
    c: null
  },
  f: [1, {
    a: 'same',
    b: [{
      a: 'same'
    }, {
      d: 'delete'
    }]
  }],
  g: new Date('2017.11.25')
}, {
  a: 'i am unchanged',
  c: 'i am created',
  e: {
    a: '1',
    b: '',
    d: 'created'
  },
  f: [{
    a: 'same',
    b: [{
      a: 'same'
    }, {
      c: 'create'
    }]
  }, 1],
  g: new Date('2017.11.25')
});
console.log(result);


3
+1それは悪いコードではありません。ただし、バグがあります(この例を確認してください:jsfiddle.net/kySNu/3 cは次のように作成されundefinedますが、文字列である必要があります'i am created')。さらに、それは、最も重要な(そして複雑で難しい)部分。'array' != typeof(obj)ちなみに、配列は配列のインスタンスであるオブジェクトであるため、構造は役に立ちません。
Martin Jespersen、2011

1
コードを更新しましたが、結果のオブジェクトに必要な値がわかりません。現在、コードは最初のオブジェクトから値を返し、存在しない場合は2番目のオブジェクトからの値がデータとして設定されます。
sbgoran

1
また、その{type: ..., data:..}オブジェクトの各インデックスに対して取得する配列の「深い配列値の比較に欠けている」とはどういう意味ですか。不足しているのは、2番目の最初の配列から値を検索することですが、私の回答で述べたように、([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}

これは、deep diffでは異なります。

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によって指摘されているように、上記は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());

完全な例+ tests + mixins については、http://jsfiddle.net/drzaus/9g5qoxwj/を参照してください


なぜ反対票を投じたのかわからないが、これは、浅くて単純な例とより複雑な深い関​​数を提供したので十分でした。
セイリア2014年

2
@Seiyria嫌いは嫌いだと思います...もともとomitは深い差分になると思っていたので両方を行いましたが、間違っていたので比較のために含めました。
drzaus 14年

1
素晴らしい解決策。で変更することをお勧めします。これによりr[k] = ... : vr[k] = ... : {'a':v, 'b':b[k] }2つの値を確認できます。
guyaloni

2
オブジェクトが他の点では同じですが、二つ目は、複数の要素、例えば持っているこれらの戻り偽陰性の両方{a:1, b:2}とを{a:1, b:2, c:3}
JJJ 2016年

1
そのはず _.omitBy代わりです_.omit
JP

48

ES6ソリューションを提供したいと思います...これは一方向の差分です。つまり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
私は解決策が好きですが、問題が1つあります。オブジェクトが1レベルより深い場合、変更されたネストされたオブジェクトのすべての値が返されます-または少なくともそれが私に起こっています。
スプリアス2017

3
うん、これは再帰的ではありません@Spurious
Nemesarial

2
このソリューションでは、オブジェクトの各要素について、配列に1つの項目を追加するだけで、既存のすべての要素がコピーされたまったく新しいオブジェクトが作成されることを覚えておいてください。小さなオブジェクトの場合は問題ありませんが、大きなオブジェクトの場合は指数関数的に遅くなります。
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);
    }
});

key / object / sourceは使用していませんが、アクセスする必要がある場合はそのままにしておきます。オブジェクトの比較は、コンソールが最も外側の要素から最も内側の要素への相違をコンソールに出力しないようにするだけです。

内部にロジックを追加して配列を処理できます。おそらく最初に配列をソートします。これは非常に柔軟なソリューションです。

編集

lodashの更新により、_。mergeから_.mergeWithに変更されました。変更を通知してくれたAvironに感謝します。


6
lodash 4.15.0では、カスタマイザ機能付きの_.mergeはサポートされなくなったため、代わりに_.mergeWithを使用する必要があります。
Aviran Cohen

1
この関数は素晴らしいですが、ネストされたオブジェクトでは機能しません。
Joe Allen、

13

以下は、2つのJavaScriptオブジェクト間の差分を見つけるために使用できるJavaScriptライブラリです。

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

NpmjsのURL: https

ブラウザだけでなくNode.jsでも再帰的差分ライブラリを使用できます。ブラウザの場合は、次の手順を実行します。

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

日付のサポートが追加されました
Anant

9

最近では、これに使用できるモジュールがかなりあります。最近、これを行うためのモジュールを作成しました。これは、見つけた多数の差分モジュールに満足できなかったためです。その名前odiffhttps : //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);

毎週50万回以上ダウンロードされる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 を使用するのと同じくらい簡単に実行できます。これは私が必要としているものではありません。それらの組み合わせではなく、2つのオブジェクトの違いが必要です。
Martin Jespersen、2011

Extがここで使用されていることは素晴らしいことです
過酸化物

2

JavaScriptで「compareValue()」という名前の関数を開発しました。値が同じかどうかを返します。1つのオブジェクトのforループでcompareValue()を呼び出しました。2つのオブジェクトの違いを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コードのtypescriptバージョンは次のとおりです

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

@sbgoranの回答を変更して、結果のdiffオブジェクトに変更された値のみが含まれ、同じ値は省略されるようにしました。また、元の値と更新された値の両方表示されます

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

ユーザーオプションとしてのオブジェクトを内部のクローンと比較するプロジェクトの1つに、すでに関数を記述しています。また、ユーザーが不正なタイプのデータを入力したり削除したりした場合、純粋なJavaScriptで検証し、デフォルト値で置き換えることもできます。

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

2つのオブジェクトの違いを取得する方法を探すために、ここでつまずきました。これは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

これが解決策です:

  • Typescript(しかし簡単にJavascriptに変換可能)
  • lib依存関係がない
  • ジェネリックであり、オブジェクトタイプ(タイプを除くobject)のチェックは不要
  • 値を持つプロパティをサポート undefined
  • 深くない(デフォルト)

まず、比較結果インターフェースを定義します。

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

古い値と新しい値が何であるかを知りたい特別な変更の場合:

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

次に、diff2つのループのみの関数を提供できます(再帰性がある場合はdeep isのはを使用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'},
}

そして、deep3番目のパラメーターで同じものを呼び出すと、次が返されます。

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