Perbedaan dalam generik antara dua objek


222

Saya memiliki dua objek: oldObjdan newObj.

Data di oldObjdigunakan untuk mengisi formulir dan newObjmerupakan hasil dari pengguna mengubah data dalam formulir ini dan mengirimkannya.

Kedua objek dalam, yaitu. mereka memiliki properti yang merupakan objek atau array objek dll - mereka dapat n level dalam, sehingga algoritma diff harus bersifat rekursif.

Sekarang saya tidak hanya perlu mencari tahu apa yang diubah (seperti pada tambah / perbarui / dihapus) dari oldObjke newObj, tetapi juga bagaimana cara terbaik untuk mewakilinya.

Sejauh ini pikiranku adalah untuk hanya membangun genericDeepDiffBetweenObjectsmetode yang akan mengembalikan objek pada formulir {add:{...},upd:{...},del:{...}}tetapi kemudian aku berpikir: orang lain pasti membutuhkan ini sebelumnya.

Jadi ... apakah ada yang tahu perpustakaan atau sepotong kode yang akan melakukan ini dan mungkin memiliki cara yang lebih baik untuk mewakili perbedaan (dengan cara yang masih berseri JSON)?

Memperbarui:

Saya telah memikirkan cara yang lebih baik untuk mewakili data yang diperbarui, dengan menggunakan struktur objek yang sama dengan newObj, tetapi mengubah semua nilai properti menjadi objek pada formulir:

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

Jadi jika newObj.prop1 = 'new value'dan oldObj.prop1 = 'old value'itu akan diaturreturnObj.prop1 = {type: 'update', data: 'new value'}

Pembaruan 2:

Itu menjadi benar-benar berbulu ketika kita sampai ke properti yang array, karena array [1,2,3]harus dihitung sama dengan [2,3,1], yang cukup sederhana untuk array tipe nilai berbasis seperti string, int & bool, tetapi menjadi sangat sulit untuk ditangani ketika datang ke array tipe referensi seperti objek dan array.

Contoh array yang harus ditemukan sama:

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

Tidak hanya cukup rumit untuk memeriksa jenis kesetaraan nilai yang dalam ini, tetapi juga untuk mencari cara yang baik untuk mewakili perubahan yang mungkin terjadi.



2
@ a'r: Ini bukan duplikat dari stackoverflow.com/questions/1200562/... - Saya tahu cara melintasi objek, saya mencari seni sebelumnya karena ini bukan hal sepele dan akan membutuhkan waktu nyata untuk diimplementasikan, dan saya Lebih baik menggunakan perpustakaan daripada membuatnya dari awal.
Martin Jespersen

1
Apakah Anda benar-benar memerlukan objek yang berbeda, apakah newObj yang dihasilkan dari server pada formulir mengirimkan respons? Karena jika Anda tidak memiliki "pembaruan server" dari suatu objek, Anda dapat menyederhanakan masalah Anda dengan melampirkan pendengar acara yang sesuai dan pada interaksi pengguna (perubahan objek) Anda dapat memperbarui / membuat daftar perubahan yang diinginkan.
sbgoran

1
@sbgoran: newObjdihasilkan oleh nilai membaca kode js dari formulir di DOM. Ada beberapa cara untuk menjaga keadaan dan melakukan ini lebih mudah, tetapi saya ingin menjadikannya tanpa kewarganegaraan sebagai latihan. Saya juga mencari seni sebelumnya untuk melihat bagaimana orang lain mungkin menangani ini, jika memang ada yang punya.
Martin Jespersen

3
inilah perpustakaan yang sangat canggih untuk melakukan diff / patch setiap objek Javascript github.com/benjamine/jsondiffpatch Anda dapat melihatnya langsung di sini: benjamine.github.io/jsondiffpatch/demo/index.html (penafian: saya penulisnya)
Benja

Jawaban:


141

Saya menulis sebuah kelas kecil yang melakukan apa yang Anda inginkan, Anda dapat mengujinya di sini .

Satu-satunya hal yang berbeda dari proposal Anda adalah bahwa saya tidak menganggapnya [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]sama, karena saya pikir array tidak sama jika urutan elemen-elemennya tidak sama. Tentu ini bisa diubah jika diperlukan. Kode ini juga dapat ditingkatkan untuk mengambil fungsi sebagai argumen yang akan digunakan untuk memformat objek diff dengan cara yang sewenang-wenang berdasarkan nilai primitif yang diteruskan (sekarang pekerjaan ini dilakukan dengan metode "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 Ini bukan bagian kode yang buruk. Namun ada bug (lihat contoh ini: jsfiddle.net/kySNu/3 c dibuat sebagai undefinedtetapi harus menjadi string 'i am created'), dan selain itu tidak melakukan apa yang saya butuhkan karena tidak memiliki nilai array mendalam dibandingkan yang merupakan bagian yang paling penting (dan kompleks / sulit). Sebagai catatan, konstruksinya 'array' != typeof(obj)tidak berguna karena array adalah objek yang merupakan instance dari array.
Martin Jespersen

1
Saya memperbarui kode, tetapi saya tidak yakin nilai apa yang Anda inginkan dalam objek yang dihasilkan, sekarang kode mengembalikan nilai dari objek pertama dan jika tidak ada nilai dari yang kedua akan ditetapkan sebagai data.
sbgoran

1
Dan bagaimana maksud Anda "kurang membandingkan nilai array mendalam" untuk array Anda akan mendapatkan untuk setiap indeks {type: ..., data:..}objek. Apa yang hilang adalah mencari nilai dari array pertama di kedua, tetapi seperti yang saya sebutkan dalam jawaban saya, saya tidak berpikir bahwa array sama jika urutan nilainya tidak sama ( [1, 2, 3] is not equal to [3, 2, 1]menurut saya).
sbgoran

6
@MartinJespersen OK, bagaimana Anda secara umum memperlakukan array ini maka: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Sekarang adalah objek pertama dalam array pertama diperbarui dengan "value1" atau "value2". Dan ini adalah contoh sederhana, ini bisa menjadi jauh lebih rumit dengan sarang yang dalam. Jika Anda ingin / perlu perbandingan mendalam bersarang terlepas dari posisi kunci tidak membuat array dari objek, membuat objek dengan objek bersarang seperti misalnya sebelumnya: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran

2
Saya setuju dengan Anda sudut pandang terakhir - struktur data asli harus diubah menjadi sesuatu yang lebih mudah untuk dilakukan perbedaan aktual. Selamat, Anda berhasil :)
Martin Jespersen

88

Menggunakan Garis Bawah, perbedaan sederhana:

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

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

Hasil di bagian o1yang sesuai tetapi dengan nilai yang berbeda di o2:

{a: 1, b: 2}

Ini akan berbeda untuk perbedaan dalam:

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

Seperti yang ditunjukkan oleh @Juhana di komentar, di atas hanya beda a -> b dan tidak reversibel (artinya properti tambahan di b akan diabaikan). Gunakan 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());

Lihat http://jsfiddle.net/drzaus/9g5qoxwj/ untuk contoh lengkap + tes + mixins


Tidak yakin mengapa Anda dipilih, ini sudah cukup karena Anda memberikan contoh yang dangkal dan sederhana serta fungsi mendalam yang lebih kompleks.
Seiyria

2
@Seiyria membenci akan membenci, saya kira ... Saya melakukan keduanya karena saya awalnya berpikir omitakan menjadi perbedaan besar, tetapi salah, jadi termasuk juga untuk perbandingan.
drzaus

1
Solusi bagus Saya akan menyarankan untuk mengubah r[k] = ... : vdalam r[k] = ... : {'a':v, 'b':b[k] }, cara ini Anda bisa melihat dua nilai.
guyaloni

2
Keduanya mengembalikan negatif palsu ketika benda-benda itu identik tetapi yang kedua memiliki lebih banyak elemen, misalnya {a:1, b:2}dan {a:1, b:2, c:3}.
JJJ

1
Seharusnya _.omitBybukan _.omit.
JP

48

Saya ingin menawarkan solusi ES6 ... Ini adalah perbedaan satu arah, artinya akan mengembalikan kunci / nilai dari o2yang tidak identik dengan rekan-rekan mereka di 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
Solusi yang bagus tetapi Anda mungkin ingin memeriksa if(o1[key] === o1[key])saluran itu Bung
bm_i

Apakah kode lengkap? Saya mulaiUncaught SyntaxError: Unexpected token ...
Seano

2
Saya suka solusinya tetapi memiliki satu masalah, jika objek lebih dalam dari satu level, itu akan mengembalikan semua nilai dalam objek bersarang yang diubah - atau setidaknya itulah yang terjadi pada saya.
Spurious

3
Yup, ini bukan rekursif @Spurious
Nemesarial

2
Hanya perlu diingat bahwa dengan solusi ini, untuk setiap elemen dalam objek Anda mendapatkan objek yang sama sekali baru dibangun dengan semua elemen yang ada disalin ke dalamnya hanya untuk menambahkan satu item ke array. Untuk objek kecil tidak masalah, tetapi akan melambat secara eksponensial untuk objek yang lebih besar.
Malvine

22

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

Saya tidak menggunakan kunci / objek / sumber tetapi saya meninggalkannya di sana jika Anda perlu mengaksesnya. Perbandingan objek hanya mencegah konsol mencetak perbedaan ke konsol dari elemen terluar ke elemen terdalam.

Anda dapat menambahkan beberapa logika di dalam untuk menangani array. Mungkin mengurutkan array terlebih dahulu. Ini adalah solusi yang sangat fleksibel.

EDIT

Berubah dari _.merge ke _.mergeDengan pembaruan lodash. Terima kasih Aviron karena memperhatikan perubahan.


6
Di lodash 4.15.0 _.merge dengan fungsi customizer tidak lagi didukung sehingga Anda harus menggunakan _.mergeWith sebagai gantinya.
Aviran Cohen

1
fungsi ini bagus tetapi tidak berfungsi di objek bersarang.
Joe Allen

13

Berikut ini adalah pustaka JavaScript yang dapat Anda gunakan untuk menemukan perbedaan antara dua objek JavaScript:

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

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

Anda dapat menggunakan pustaka rekursif-diff di browser dan juga Node.js. Untuk browser, lakukan hal berikut:

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

Sedangkan di node.js Anda dapat memerlukan modul 'recursive-diff' dan menggunakannya seperti di bawah ini:

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

Ini tidak akan menjelaskan perubahan dalam properti Date, misalnya.
trollkotze

tanggal dukungan ditambahkan
Anant

9

Saat ini, ada beberapa modul yang tersedia untuk ini. Baru-baru ini saya menulis modul untuk melakukan ini, karena saya tidak puas dengan berbagai modul berbeda yang saya temukan. Namanya odiff: https://github.com/Tixit/odiff . Saya juga mendaftar banyak modul yang paling populer dan mengapa mereka tidak dapat diterima di readme of odiff, yang dapat Anda lihat jika odifftidak memiliki properti yang Anda inginkan. Ini sebuah contoh:

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

Ada modul npm dengan lebih dari 500k unduhan mingguan: https://www.npmjs.com/package/deep-object-diff

Saya suka objek seperti representasi perbedaan - terutama mudah untuk melihat strukturnya, ketika sudah diformulasikan.

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

Saya telah menggunakan kode ini untuk melakukan tugas yang Anda jelaskan:

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

ini akan memberi Anda objek baru yang akan menggabungkan semua perubahan antara objek lama dan objek baru dari formulir Anda


1
Saya menggunakan kerangka kerja Ext di sini tetapi Anda dapat menggantinya dan menggunakan kerangka apa pun yang Anda inginkan ...
AMember

Menggabungkan objek itu sepele dan bisa dilakukan semudah $.extend(true,obj1,obj2)menggunakan jQuery. Ini sama sekali bukan yang saya butuhkan. Saya perlu perbedaan antara dua objek bukan kombinasi keduanya.
Martin Jespersen

hebatnya Ext digunakan di sini
peroksida

2

Saya telah mengembangkan Fungsi bernama "compareValue ()" dalam Javascript. mengembalikan apakah nilainya sama atau tidak. Saya telah memanggil compareValue () di untuk loop dari satu Objek. Anda bisa mendapatkan perbedaan dua objek dalam 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

Saya tahu saya terlambat ke pesta, tetapi saya membutuhkan sesuatu yang serupa sehingga jawaban di atas tidak membantu.

Saya menggunakan fungsi $ watch Angular untuk mendeteksi perubahan dalam suatu variabel. Saya tidak hanya perlu tahu apakah properti telah berubah pada variabel, tetapi saya juga ingin memastikan bahwa properti yang berubah bukan bidang sementara yang dihitung. Dengan kata lain, saya ingin mengabaikan sifat-sifat tertentu.

Berikut kodenya: https://jsfiddle.net/rv01x6jo/

Berikut cara menggunakannya:

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

Semoga ini bisa membantu seseorang.


Harap sertakan kode dalam jawaban Anda juga, bukan hanya biola.
xpy

Sepertinya defineProperty akan menyelesaikan masalah ini dengan kinerja yang lebih baik, jika saya ingat dengan benar itu bekerja sampai ke IE9.
Peter

Terima kasih..!! Kode Anda berfungsi seperti pesona dan menyelamatkan hari saya. Saya memiliki objek json 1250 baris dan itu memberi saya tepat o / p yang saya inginkan.
Tejas Mehta

1

Saya hanya menggunakan ramda, untuk menyelesaikan masalah yang sama, saya perlu tahu apa yang diubah pada objek baru. Jadi di sini desain saya.

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)

hasilnya adalah, nama properti dan statusnya.

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

1

Ini adalah versi naskah dari kode @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

Ini adalah versi modifikasi dari sesuatu yang ditemukan di 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

Saya memodifikasi jawaban @ sbgoran sehingga objek diff yang dihasilkan hanya mencakup nilai yang diubah , dan menghilangkan nilai yang sama. Selain itu, ini menunjukkan nilai asli dan nilai yang diperbarui .

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

Saya sudah menulis fungsi untuk salah satu proyek saya yang akan membandingkan objek sebagai opsi pengguna dengan klon internal. Itu juga dapat memvalidasi dan bahkan mengganti dengan nilai default jika pengguna memasukkan tipe data yang buruk atau dihapus, dalam javascript murni.

Dalam IE8 100% berfungsi. Diuji berhasil.

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

/ * hasil

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

Fungsi lebih luas dan disederhanakan dari jawaban sbgoran.
Ini memungkinkan pemindaian mendalam dan menemukan kesamaan array.

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

Saya tersandung di sini mencoba mencari cara untuk mendapatkan perbedaan antara dua objek. Ini solusi saya menggunakan 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

Cuplikan kode di bawah ini:

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

Saya mengambil jawaban di atas oleh @sbgoran dan memodifikasinya untuk kasus saya sama dengan pertanyaan yang diperlukan, untuk memperlakukan array sebagai set (mis. Pesanan tidak penting untuk 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

Berikut ini solusinya yaitu:

  • Naskah (tapi mudah dikonversi ke Javascript)
  • tidak memiliki dependensi lib
  • generik, dan tidak peduli memeriksa jenis objek (selain objecttipe)
  • mendukung properti dengan nilai undefined
  • dalam tidak (default)

Pertama-tama kita mendefinisikan antarmuka hasil perbandingan:

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

dengan kasus perubahan khusus di mana kami ingin tahu apa yang lama dan nilai-nilai baru:

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

Kemudian kita dapat menyediakan difffungsi yang hanya dua loop (dengan recursivity if deepis 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';
  }
}

Sebagai contoh, menelepon:

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

akan kembali:

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

dan memanggil yang sama dengan deepparameter ketiga akan kembali:

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

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.