Chung khác biệt sâu giữa hai đối tượng


222

Tôi có hai đối tượng: oldObjnewObj.

Dữ liệu trong oldObjđược sử dụng để điền vào biểu mẫu và newObjlà kết quả của việc người dùng thay đổi dữ liệu trong biểu mẫu này và gửi nó.

Cả hai đối tượng đều sâu, tức là. chúng có các thuộc tính là các đối tượng hoặc mảng của các đối tượng, v.v. - chúng có thể sâu n cấp, do đó thuật toán diff cần phải được đệ quy.

Bây giờ tôi cần không chỉ là tìm ra những gì đã thay đổi (như trong thêm / cập nhật / xóa) từ oldObjđến newObj, mà còn làm thế nào để tốt nhất đại diện cho nó.

Cho đến nay, suy nghĩ của tôi là chỉ xây dựng một genericDeepDiffBetweenObjectsphương thức sẽ trả về một đối tượng trên biểu mẫu {add:{...},upd:{...},del:{...}}nhưng sau đó tôi nghĩ: ai đó phải có điều này trước đây.

Vậy ... có ai biết về một thư viện hoặc một đoạn mã sẽ làm điều này và có thể có cách thể hiện sự khác biệt thậm chí tốt hơn (theo cách vẫn là tuần tự hóa JSON)?

Cập nhật:

Tôi đã nghĩ ra một cách tốt hơn để biểu diễn dữ liệu được cập nhật, bằng cách sử dụng cùng cấu trúc đối tượng như newObj, nhưng biến tất cả các giá trị thuộc tính thành các đối tượng trên biểu mẫu:

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

Vì vậy, nếu newObj.prop1 = 'new value'oldObj.prop1 = 'old value'nó sẽ thiết lậpreturnObj.prop1 = {type: 'update', data: 'new value'}

Cập nhật 2:

Nó thực sự có lông khi chúng ta có các thuộc tính là mảng, vì mảng [1,2,3]phải được tính bằng [2,3,1], đủ đơn giản cho các mảng của các loại dựa trên giá trị như chuỗi, int & bool, nhưng thực sự khó xử lý khi nói đến mảng các kiểu tham chiếu như đối tượng và mảng.

Các mảng ví dụ cần được tìm thấy bằng nhau:

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

Không chỉ khá phức tạp để kiểm tra loại bình đẳng giá trị sâu này, mà còn tìm ra một cách tốt để thể hiện những thay đổi có thể xảy ra.



2
@ a'r: Nó không phải là bản sao của stackoverflow.com/questions/1200562/NH - Tôi biết cách vượt qua các đối tượng, tôi đang tìm kiếm nghệ thuật trước vì điều này không tầm thường và sẽ mất thời gian thực để thực hiện, và tôi Thà sử dụng một thư viện hơn là làm cho nó từ đầu.
Martin Jespersen

1
Bạn có thực sự cần khác biệt của các đối tượng, đó có phải là newObj được tạo từ máy chủ trên biểu mẫu gửi phản hồi không? Bởi vì nếu bạn không có "cập nhật máy chủ" của một đối tượng, bạn có thể đơn giản hóa vấn đề của mình bằng cách đính kèm các trình lắng nghe sự kiện phù hợp và khi người dùng tương tác (thay đổi đối tượng), bạn có thể cập nhật / tạo danh sách thay đổi mong muốn.
sbgoran

1
@sbgoran: newObjđược tạo bởi các giá trị đọc mã js từ một biểu mẫu trong DOM. Có một số cách để giữ trạng thái và thực hiện việc này dễ dàng hơn nhiều, nhưng tôi muốn giữ trạng thái không trạng thái như một bài tập. Ngoài ra tôi đang tìm kiếm nghệ thuật trước để xem những người khác có thể đã giải quyết điều này như thế nào, nếu thực sự có ai đó.
Martin Jespersen

3
đây là một thư viện rất phức tạp để tìm / vá bất kỳ cặp đối tượng Javascript nào github.com/benjamine/jsondiffpatch bạn có thể thấy nó ở đây: benjamine.github.io/jsondiffpatch/demo/index.html (từ chối trách nhiệm: Tôi là tác giả)
Benja

Câu trả lời:


141

Tôi đã viết một lớp học nhỏ đang làm những gì bạn muốn, bạn có thể kiểm tra nó ở đây .

Điều duy nhất khác với đề xuất của bạn là tôi không coi [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]là giống nhau, vì tôi nghĩ rằng các mảng không bằng nhau nếu thứ tự các phần tử của chúng không giống nhau. Tất nhiên điều này có thể được thay đổi nếu cần thiết. Ngoài ra, mã này có thể được tăng cường hơn nữa để lấy chức năng làm đối số sẽ được sử dụng để định dạng đối tượng diff theo cách tùy ý dựa trên các giá trị nguyên thủy được thông qua (bây giờ công việc này được thực hiện bằng phương thức "soValues").

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 Đây không phải là một đoạn mã xấu. Tuy nhiên, có một lỗi (kiểm tra ví dụ này: jsfiddle.net/kySNu/3 c được tạo ra undefinednhưng phải là chuỗi 'i am created'), và bên cạnh đó nó không làm những gì tôi cần vì nó thiếu giá trị mảng sâu so sánh đó là phần quan trọng nhất (và phức tạp / khó khăn). Như một lưu ý phụ, cấu trúc 'array' != typeof(obj)là vô dụng vì các mảng là các đối tượng là các thể hiện của mảng.
Martin Jespersen

1
Tôi đã cập nhật mã, nhưng tôi không chắc giá trị nào bạn muốn trong đối tượng kết quả, ngay bây giờ mã đang trả về giá trị từ đối tượng đầu tiên và nếu nó không tồn tại giá trị từ thứ hai sẽ được đặt làm dữ liệu.
sbgoran

1
Và làm thế nào để bạn có nghĩa là "thiếu so sánh giá trị mảng sâu" cho các mảng bạn sẽ nhận được cho mỗi chỉ mục {type: ..., data:..}đối tượng đó . Điều còn thiếu là tìm kiếm giá trị từ mảng thứ nhất trong giây, nhưng như tôi đã đề cập trong câu trả lời của mình, tôi không nghĩ rằng các mảng bằng nhau nếu thứ tự các giá trị của chúng không bằng nhau ( [1, 2, 3] is not equal to [3, 2, 1]theo ý kiến ​​của tôi).
sbgoran

6
@MartinJespersen OK, bạn sẽ xử lý chung mảng này như thế nào : [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Bây giờ là đối tượng đầu tiên trong mảng đầu tiên được cập nhật với "value1" hoặc "value2". Và đây là ví dụ đơn giản, nó có thể trở nên phức tạp hơn với việc làm tổ sâu. Nếu bạn muốn / cần so sánh lồng nhau sâu bất kể vị trí chính không tạo ra các mảng đối tượng, hãy tạo các đối tượng với các đối tượng lồng nhau như ví dụ trước : {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran

2
Tôi đồng ý với quan điểm cuối cùng của bạn - cấu trúc dữ liệu ban đầu nên được thay đổi thành một thứ dễ dàng hơn để thực hiện một khác biệt thực tế trên. Xin chúc mừng, bạn đã đóng đinh nó :)
Martin Jespersen

88

Sử dụng Underscore, một khác biệt đơn giản:

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

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

Kết quả trong các phần o1tương ứng nhưng với các giá trị khác nhau trong o2:

{a: 1, b: 2}

Nó sẽ khác với một khác biệt sâu sắc:

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

Như @Juhana đã chỉ ra trong các bình luận, ở trên chỉ là một khác biệt a -> b và không thể đảo ngược (có nghĩa là các thuộc tính bổ sung trong b sẽ bị bỏ qua). Sử dụng thay thế 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());

Xem http://jsfiddle.net/drzaus/9g5qoxwj/ để biết ví dụ đầy đủ + bài kiểm tra + mixins


Không chắc chắn tại sao bạn bị hạ cấp, điều này là đủ khi bạn cung cấp một ví dụ đơn giản, nông cạn cũng như một hàm sâu phức tạp hơn.
Seiyria

2
Những người ghét @Seiyria sẽ ghét, tôi đoán ... Tôi đã làm cả hai vì ban đầu tôi nghĩ omitsẽ là một khác biệt sâu sắc, nhưng đã sai, vì vậy cũng bao gồm để so sánh.
drzaus

1
Giải pháp tốt đẹp. Tôi sẽ đề nghị thay đổi r[k] = ... : vtrong r[k] = ... : {'a':v, 'b':b[k] }cách này bạn có thể nhìn thấy hai giá trị.
Guyaloni

2
Cả hai đều trả về một phủ định sai khi các đối tượng giống hệt nhau nhưng đối tượng thứ hai có nhiều phần tử hơn, ví dụ {a:1, b:2}{a:1, b:2, c:3}.
JJJ

1
Nó nên được _.omitBythay vì _.omit.
JP

48

Tôi muốn cung cấp một giải pháp ES6 ... Đây là một khác biệt một chiều, có nghĩa là nó sẽ trả về các khóa / giá trị từ o2đó không giống với các đối tác của chúng trong 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
Giải pháp hay nhưng bạn có thể muốn kiểm tra if(o1[key] === o1[key])anh chàng đó
bm_i

Mã đã hoàn thành chưa? Tôi đang nhận đượcUncaught SyntaxError: Unexpected token ...
Seano

2
Tôi thích giải pháp nhưng nó có một vấn đề, nếu đối tượng sâu hơn một cấp, nó sẽ trả về tất cả các giá trị trong các đối tượng lồng nhau đã thay đổi - hoặc ít nhất đó là những gì xảy ra với tôi.
Giả mạo

3
Phải, đây không phải là đệ quy @Spurdy
Nemearial

2
Hãy nhớ rằng với giải pháp này, với mỗi phần tử trong đối tượng, bạn sẽ có một đối tượng hoàn toàn mới được xây dựng với tất cả các phần tử hiện có được sao chép vào nó chỉ để thêm một mục vào mảng. Đối với các vật thể nhỏ thì không sao, nhưng nó sẽ chậm lại theo cấp số nhân đối với các vật thể lớn hơn.
Malvineous

22

Sử dụng 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);
    }
});

Tôi không sử dụng khóa / đối tượng / nguồn nhưng tôi để nó ở đó nếu bạn cần truy cập chúng. Việc so sánh đối tượng chỉ ngăn giao diện điều khiển in sự khác biệt với bàn điều khiển từ phần tử ngoài cùng đến phần tử trong cùng.

Bạn có thể thêm một số logic bên trong để xử lý mảng. Có lẽ sắp xếp các mảng đầu tiên. Đây là một giải pháp rất linh hoạt.

BIÊN TẬP

Đã thay đổi từ _.merge thành _.mergeVới cập nhật lodash. Cảm ơn Aviron đã nhận thấy sự thay đổi.


6
Trong lodash 4.15.0 _.merge với chức năng tùy biến không còn được hỗ trợ, vì vậy bạn nên sử dụng _.mergeWith thay thế.
Aviran Cohen

1
chức năng này là tuyệt vời nhưng không hoạt động trong đối tượng lồng nhau.
Joe Allen

13

Đây là thư viện JavaScript mà bạn có thể sử dụng để tìm khác biệt giữa hai đối tượng JavaScript:

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

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

Bạn có thể sử dụng thư viện đệ quy-diff trong trình duyệt cũng như Node.js. Đối với trình duyệt, hãy làm như sau:

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

Trong khi đó trong node.js, bạn có thể yêu cầu mô đun 'đệ quy-diff' và sử dụng nó như dưới đây:

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

Điều này sẽ không tính đến những thay đổi trong thuộc tính Date chẳng hạn.
trollkotze

hỗ trợ ngày được thêm vào
Anant

9

Ngày nay, có khá nhiều mô-đun có sẵn cho việc này. Gần đây tôi đã viết một mô-đun để làm điều này, bởi vì tôi không hài lòng với nhiều mô-đun khác nhau mà tôi tìm thấy. Nó được gọi là odiff: https://github.com/Tixit/odiff . Tôi cũng đã liệt kê một loạt các mô-đun phổ biến nhất và tại sao chúng không được chấp nhận trong phần đọc odiff, mà bạn có thể xem qua nếu odiffkhông có các thuộc tính bạn muốn. Đây là một ví dụ:

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

Có một mô-đun npm với hơn 500 nghìn lượt tải xuống hàng tuần: https://www.npmjs.com/package/deep-object-diff

Tôi thích đối tượng như đại diện cho sự khác biệt - đặc biệt là dễ dàng nhìn thấy cấu trúc, khi nó được hình thành.

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

Tôi đã sử dụng đoạn mã này để thực hiện nhiệm vụ mà bạn mô tả:

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

điều này sẽ giúp bạn có một đối tượng mới sẽ hợp nhất tất cả các thay đổi giữa đối tượng cũ và đối tượng mới từ biểu mẫu của bạn


1
Tôi đang sử dụng khuôn khổ Ext đây nhưng bạn có thể thay thế nó và sử dụng những gì từng khuôn khổ khác mà bạn muốn ...
aMember

Việc hợp nhất các đối tượng là tầm thường và có thể được thực hiện dễ dàng như $.extend(true,obj1,obj2)sử dụng jQuery. Đây không phải là tất cả những gì tôi cần. Tôi cần sự khác biệt giữa hai đối tượng không phải là sự kết hợp của chúng.
Martin Jespersen

thật tuyệt khi Ext được sử dụng ở đây
peroxide

2

Tôi đã phát triển Hàm có tên "soValue ()" trong Javascript. nó trả về giá trị có giống nhau hay không. Tôi đã gọi so sánhValue () trong vòng lặp của một Đối tượng. bạn có thể nhận được sự khác biệt của hai đối tượng trong 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

Tôi biết tôi đến bữa tiệc muộn, nhưng tôi cần một cái gì đó tương tự mà các câu trả lời ở trên không giúp được gì.

Tôi đã sử dụng chức năng $ watch của Angular để phát hiện các thay đổi trong một biến. Tôi không chỉ cần biết liệu một thuộc tính có thay đổi trên biến hay không, mà tôi còn muốn đảm bảo rằng thuộc tính đó không phải là trường được tính tạm thời. Nói cách khác, tôi muốn bỏ qua các thuộc tính nhất định.

Đây là mã: https://jsfiddle.net/rv01x6jo/

Đây là cách sử dụng nó:

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

Hy vọng điều này sẽ giúp được ai đó.


Vui lòng bao gồm mã trong câu trả lời của bạn, không chỉ là một câu đố.
xpy

Có vẻ như notifyProperty sẽ giải quyết vấn đề này với hiệu suất tốt hơn, nếu tôi nhớ chính xác thì nó hoạt động hoàn toàn xuống IE9.
Peter

Cảm ơn..!! Mã của bạn hoạt động như sự quyến rũ và lưu lại ngày của tôi. Tôi có đối tượng json gồm 1250 dòng và nó cho tôi chính xác o / p mà tôi muốn.
Tejas Mehta

1

Tôi chỉ sử dụng ramda, để giải quyết vấn đề tương tự, tôi cần biết những gì được thay đổi trong đối tượng mới. Vì vậy, đây là thiết kế của tôi.

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)

kết quả là, tên của tài sản và tình trạng của nó.

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

1

Đây là phiên bản bản thảo của mã @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

Đây là một phiên bản sửa đổi của một cái gì đó được tìm thấy trên 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

Tôi đã sửa đổi câu trả lời của @ sbgoran để đối tượng diff thu được chỉ bao gồm các giá trị đã thay đổi và bỏ qua các giá trị giống nhau. Ngoài ra, nó hiển thị cả giá trị ban đầu và giá trị cập nhật .

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

Tôi đã viết một hàm cho một trong các dự án của mình, nó sẽ so sánh một đối tượng dưới dạng tùy chọn người dùng với bản sao bên trong của nó. Nó cũng có thể xác nhận và thậm chí thay thế bằng các giá trị mặc định nếu người dùng nhập loại dữ liệu xấu hoặc bị xóa, trong javascript thuần.

Trong IE8 hoạt động 100%. Đã thử nghiệm thành công.

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

/* kết quả

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

Hàm mở rộng và đơn giản hơn từ câu trả lời của sbgoran.
Điều này cho phép quét sâu và tìm sự tương đồng của một mảng.

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

Tôi vấp ngã ở đây cố gắng tìm cách để có được sự khác biệt giữa hai đối tượng. Đây là giải pháp của tôi bằng cách sử dụng 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

Đoạn mã dưới đây:

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

Tôi đã lấy câu trả lời ở trên bởi @sbgoran và sửa đổi nó cho trường hợp của tôi giống như câu hỏi cần thiết, để coi mảng là tập hợp (tức là thứ tự không quan trọng đối với 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

Đây là một giải pháp đó là:

  • Bản thảo (nhưng dễ dàng chuyển đổi sang Javascript)
  • không có phụ thuộc lib
  • chung chung và không quan tâm đến việc kiểm tra các loại đối tượng (ngoài objectloại)
  • hỗ trợ các thuộc tính có giá trị undefined
  • sâu không (mặc định)

Đầu tiên chúng ta xác định giao diện kết quả so sánh:

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

với trường hợp đặc biệt của sự thay đổi nơi chúng tôi muốn biết đâu là giá trị cũ và mới:

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

Sau đó, chúng ta có thể cung cấp diffhàm chỉ đơn thuần là hai vòng lặp (với đệ quy nếu deeptrue):

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

Ví dụ, gọi:

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

sẽ trở lại:

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

và gọi tương tự với deeptham số thứ ba sẽ trả về:

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.