Родова глибока різниця між двома об’єктами


222

У мене два об’єкти: oldObjіnewObj .

Дані в oldObjвикористовувались для заповнення форми таnewObj є результатом того, що користувач міняє дані у цій формі та надсилає їх.

Обидва об’єкти глибокі, тобто. у них є властивості, які є об'єктами або масивами об'єктів тощо - вони можуть бути n рівнів глибокими, тому алгоритм diff повинен бути рекурсивним.

Тепер мені потрібно не тільки зрозуміти, що змінилися (як в додаванні / оновленні / видаленні) від oldObjдоnewObj , а й як краще представити його.

Поки мої думки полягали в тому, щоб просто побудувати 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], що досить просто для масивів типів на основі значень, таких як string, int & bool, але стає важко обробляти, коли справа доходить до масиви посилальних типів, як об'єкти та масиви.

Приклад масивів, які слід знайти рівними:

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

Це не тільки досить складно перевірити наявність цього виду глибокої рівності цінностей, але й розробити хороший спосіб представити зміни, які можуть бути.



2
@ a'r: Це не дублікат stackoverflow.com/questions/1200562/… - я знаю, як перетинати об'єкти, я шукаю попереднє мистецтво, оскільки це не тривіально і знадобиться в реальному часі на реалізацію, і я скоріше використовувати бібліотеку, ніж робити її з нуля.
Мартін Єсперсен

1
Вам справді потрібні різні об’єкти, чи newObj, що генерується з сервера на формі, подає відповідь? Тому що якщо у вас немає "оновлень сервера" об'єкта, ви можете спростити свою проблему, приєднавши відповідних слухачів подій, і після взаємодії з користувачем (зміна об'єкта) ви можете оновити / створити список потрібних змін.
sbgoran

1
@sbgoran: newObjформується значеннями зчитування коду js з форми в DOM. Є кілька способів зберегти державу і зробити це набагато простіше, але я хотів би тримати це без громадянства як вправу. Крім того, я шукаю попереднього рівня техніки, щоб побачити, як інші, можливо, вирішили це, якщо справді хтось має.
Мартін Єсперсен

3
ось дуже складна бібліотека для розмежування / виправлення будь-якої пари об’єктів Javascript github.com/benjamine/jsondiffpatch, ви можете побачити її тут: benjamine.github.io/jsondiffpatch/demo/index.html (відмова від відповідальності: я автор)
Бенджа

Відповіді:


142

Я написав невеликий клас, який робить те, що ви хочете, ви можете перевірити це тут .

Єдине, що відрізняється від вашої пропозиції, це те, що я не вважаю [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)марна, оскільки масиви - це об'єкти, які є екземплярами масивів.
Мартін Єсперсен

1
Я оновив код, але я не впевнений, яке значення ви хочете в отриманому об'єкті, зараз код повертає значення з першого об'єкта, і якщо його немає, значення з другого буде встановлено як дані.
sbgoran

1
А як ви маєте на увазі "відсутність порівняння значення глибокого масиву" для масивів, які ви отримаєте для кожного індексу цього {type: ..., data:..}об'єкта. Не вистачає пошуку значень з першого масиву в другому, але, як я вже згадував у своїй відповіді, я не думаю, що масиви рівні, якщо порядок їх значень не є рівним ( [1, 2, 3] is not equal to [3, 2, 1]на мою думку).
sbgoran

6
@MartinJespersen КІ, як би ви в цілому ставитися до цим масивів , то: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Тепер перший об’єкт у першому масиві оновлюється "value1" або "value2". І це простий приклад, він може значно ускладнитися при глибокому вкладенні. Якщо ви хочете / потрібно глибоко вкладеність порівняння незалежно від положення ключа не створювати масиви об'єктів, створювати об'єкти з вкладеними об'єктами , як для попереднього прикладу: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran

2
Я згоден з вашою останньою точкою зору - оригінальну структуру даних слід змінити на щось, простіше зробити фактичне розходження. З повагою, ви прибили це :)
Мартін Єсперсен

88

Використовуючи підкреслення, простий розбіжність:

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

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

Результати в частинах o1цього відповідають, але мають різні значення у o2:

{a: 1, b: 2}

Для глибокої різниці було б інакше:

function diff(a,b) {
    var r = {};
    _.each(a, function(v,k) {
        if(b[k] === v) return;
        // but what if it returns an empty object? still attach?
        r[k] = _.isObject(v)
                ? _.diff(v, b[k])
                : v
            ;
        });
    return r;
}

Як вказував @Juhana в коментарях, вищесказане є лише різницею -> 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());

Дивіться http://jsfiddle.net/drzaus/9g5qoxwj/ для повного прикладу + тести + міксини


Не впевнений, чому ви прихильнилися, цього було достатньо, оскільки ви надали неглибокий, простий приклад, а також більш складну глибоку функцію.
Seiyria

2
@ Seeyria ненавидить ненавидіти, я думаю ... я зробив і те, і тому, що спочатку я вважав, що omitце буде глибоким розбігом, але помилявся, тому включав також для порівняння.
drzaus

1
Приємне рішення. Я хотів би запропонувати , щоб змінити r[k] = ... : vв r[k] = ... : {'a':v, 'b':b[k] }, таким чином , ви можете побачити два значення.
гайялоні

2
Обидва вони повертають помилковий негатив, коли об'єкти інакше однакові, але другий має більше елементів, наприклад, {a:1, b:2}та {a:1, b:2, c:3}.
JJJ

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 ...
Сеано

2
Мені подобається рішення, але у нього є одна проблема, якщо об’єкт глибший, ніж один рівень, він поверне всі значення змінених вкладених об'єктів - або, принаймні, це відбувається для мене.
Грізний

3
Так, це не рекурсивно @Spurious
Nemesarial

2
Просто майте на увазі, що за допомогою цього рішення для кожного елемента в об’єкті ви отримуєте абсолютно новий об’єкт, побудований із усіма скопійованими в нього всі існуючими елементами лише для додавання одного елемента до масиву. Для невеликих предметів це добре, але він уповільнюється експоненціально для більших об'єктів.
Malvineous

22

Використання Лодаша:

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

Я не використовую ключ / об’єкт / джерело, але я залишив його там, якщо вам потрібно отримати доступ до них. Порівняння об'єктів просто заважає консолі друкувати відмінності на консолі від самого зовнішнього елемента до самого внутрішнього елемента.

Ви можете додати всередину логіки для обробки масивів. Можливо, спочатку сортуйте масиви. Це дуже гнучке рішення.

EDIT

Змінено з _.merge на _.mergeWith через оновлення lodash. Дякуємо Aviron за те, що помітили зміни.


6
У lodash 4.15.0 _.merge з функцією налаштування більше не підтримується, тому слід використовувати _.mergeWith.
Авіран Коен

1
ця функція чудова, але не працює в вкладеному об'єкті.
Джо Аллен

13

Ось бібліотека JavaScript, яку ви можете використовувати для пошуку різниць між двома об’єктами JavaScript:

URL-адреса Github: https://github.com/cosmicanant/recursive-diff

URL-адреса Npmjs: https://www.npmjs.com/package/recursive-diff

Ви можете використовувати рекурсивно-різну бібліотеку в браузері, а також 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 ви можете вимагати модуль "рекурсивно-різний" і використовувати його, як показано нижче:

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

Наприклад, це не враховує змін у властивостях Date.
trollkotze

підтримка дати додана
Анант

9

Сьогодні для цього доступно досить багато модулів. Нещодавно я написав модуль для цього, оскільки не був задоволений численними різними модулями, які знайшов. Його називають odiff: https://github.com/Tixit/odiff . Я також перерахував купу найпопулярніших модулів, і чому вони не були прийнятними в режимі readme odiff, на який ви могли б ознайомитися, якщо odiffне має потрібних властивостей. Ось приклад:

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

var diffs = odiff(a,b)

/* diffs now contains:
[{type: 'add', path:[], index: 2, vals: [{t:9,y:9,u:9}]},
 {type: 'set', path:[1,'y'], val: '3'},
 {type: 'add', path:[], index: 1, vals: [{t:4,y:5,u:6}]}
]
*/

7
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

Існує модуль npm з завантаженням понад 500 кб на тиждень: 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, але ви можете її замінити і використовувати будь-який інший фреймворк, який ви хотіли б ...
квітня

Об'єднання об'єднань є тривіальними і їх можна зробити так само просто, як і $.extend(true,obj1,obj2)використовувати jQuery. Це зовсім не те, що мені потрібно. Мені потрібна різниця між двома об’єктами, а не їх поєднання.
Мартін Єсперсен

чудово, що Ext тут використовується
перекис

2

Я розробив функцію під назвою "CompareValue ()" у Javascript. він повертає, чи є значення однаковим чи ні. Я закликав CompareValue () для циклу одного Об'єкта. Ви можете отримати різницю двох об'єктів у diffParams.

var diffParams = {};
var obj1 = {"a":"1", "b":"2", "c":[{"key":"3"}]},
    obj2 = {"a":"1", "b":"66", "c":[{"key":"55"}]};

for( var p in obj1 ){
  if ( !compareValue(obj1[p], obj2[p]) ){
    diffParams[p] = obj1[p];
  }
}

function compareValue(val1, val2){
  var isSame = true;
  for ( var p in val1 ) {

    if (typeof(val1[p]) === "object"){
      var objectValue1 = val1[p],
          objectValue2 = val2[p];
      for( var value in objectValue1 ){
        isSame = compareValue(objectValue1[value], objectValue2[value]);
        if( isSame === false ){
          return false;
        }
      }
    }else{
      if(val1 !== val2){
        isSame = false;
      }
    }
  }
  return isSame;
}
console.log(diffParams);


1

Я знаю, що я спізнююсь на вечірку, але мені було потрібно щось подібне, що вищезгадані відповіді не допомогли.

Я використовував функцію $ watch Angular для виявлення змін змінної. Мені не тільки було потрібно знати, чи змінилась властивість змінної, але я також хотів переконатися, що властивість, що змінилася, не є тимчасовим, обчисленим полем. Іншими словами, я хотів ігнорувати певні властивості.

Ось код: 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.
Пітер

Дякую..!! Ваш код працює як шарм і врятував мені день. У мене є об'єкт json з 1250 рядків, і це дає мені точний o / p, який я хочу.
Мехта

1

Я просто використовую ramda, для вирішення тієї ж проблеми, мені потрібно знати, що змінилося в новому об'єкті. Тож ось мій дизайн.

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

const keysObj1 = R.keys(newState)

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

const result = R.map(filterFunc, keysObj1)

Результатом є, назва власності та його статус.

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

1

Ось версія друкарської версії коду @sbgoran

export class deepDiffMapper {

  static VALUE_CREATED = 'created';
  static VALUE_UPDATED = 'updated';
  static VALUE_DELETED = 'deleted';
  static VALUE_UNCHANGED ='unchanged';

  protected isFunction(obj: object) {
    return {}.toString.apply(obj) === '[object Function]';
  };

  protected isArray(obj: object) {
      return {}.toString.apply(obj) === '[object Array]';
  };

  protected isObject(obj: object) {
      return {}.toString.apply(obj) === '[object Object]';
  };

  protected isDate(obj: object) {
      return {}.toString.apply(obj) === '[object Date]';
  };

  protected isValue(obj: object) {
      return !this.isObject(obj) && !this.isArray(obj);
  };

  protected compareValues (value1: any, value2: any) {
    if (value1 === value2) {
        return deepDiffMapper.VALUE_UNCHANGED;
    }
    if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
        return deepDiffMapper.VALUE_UNCHANGED;
    }
    if ('undefined' == typeof(value1)) {
        return deepDiffMapper.VALUE_CREATED;
    }
    if ('undefined' == typeof(value2)) {
        return deepDiffMapper.VALUE_DELETED;
    }

    return deepDiffMapper.VALUE_UPDATED;
  }

  public map(obj1: object, obj2: object) {
      if (this.isFunction(obj1) || this.isFunction(obj2)) {
          throw 'Invalid argument. Function given, object expected.';
      }
      if (this.isValue(obj1) || this.isValue(obj2)) {
          return {
              type: this.compareValues(obj1, obj2),
              data: (obj1 === undefined) ? obj2 : obj1
          };
      }

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

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

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

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

      return diff;

  }
}

1

Ось модифікована версія чогось знайденого на gisthub .

isNullBlankOrUndefined = function (o) {
    return (typeof o === "undefined" || o == null || o === "");
}

/**
 * Deep diff between two object, using lodash
 * @param  {Object} object Object compared
 * @param  {Object} base   Object to compare with
 * @param  {Object} ignoreBlanks will not include properties whose value is null, undefined, etc.
 * @return {Object}        Return a new object who represent the diff
 */
objectDifference = function (object, base, ignoreBlanks = false) {
    if (!lodash.isObject(object) || lodash.isDate(object)) return object            // special case dates
    return lodash.transform(object, (result, value, key) => {
        if (!lodash.isEqual(value, base[key])) {
            if (ignoreBlanks && du.isNullBlankOrUndefined(value) && isNullBlankOrUndefined( base[key])) return;
            result[key] = lodash.isObject(value) && lodash.isObject(base[key]) ? objectDifference(value, base[key]) : value;
        }
    });
}

1

Я змінив відповідь @ 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

Я вже написав функцію для одного з моїх проектів, який буде порівнювати об'єкт як параметри користувача з його внутрішнім клоном. Він також може перевірити та навіть замінити значення за замовчуванням, якщо користувач ввів неправильний тип даних або видалив, у чистому 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

Більш розширена і спрощена функція від відповіді Сбгорана.
Це дозволяє глибоко сканувати та знаходити схожість масиву.

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

Я спіткнувся тут, намагаючись шукати спосіб отримати різницю між двома об’єктами. Це моє рішення за допомогою Лодаша:

// 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 і змінив її для мого випадку так само, як і потрібне питання, щоб розглядати масиви як набори (тобто порядок не важливий для розбіжностей)

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

Ось таке рішення:

  • Машинопис (але легко перетворюється в Javascript)
  • не мають залежності від ліб
  • загальний і не переймається перевіркою типів об'єктів (окрім object типу)
  • підтримує властивості зі значенням undefined
  • глибоко не (за замовчуванням)

Спочатку визначимо інтерфейс результатів порівняння:

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

з особливим випадком змін, коли ми хочемо знати, що є старими та новими значеннями:

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

Тоді ми можемо надати diffфункцію, яка становить лише дві петлі (з рекурсивністю, якщо deepє true):

export class ObjectUtils {

  static diff(o1: {}, o2: {}, deep = false): ObjectComparison {
    const added = {};
    const updated = {};
    const removed = {};
    const unchanged = {};
    for (const prop in o1) {
      if (o1.hasOwnProperty(prop)) {
        const o2PropValue = o2[prop];
        const o1PropValue = o1[prop];
        if (o2.hasOwnProperty(prop)) {
          if (o2PropValue === o1PropValue) {
            unchanged[prop] = o1PropValue;
          } else {
            updated[prop] = deep && this.isObject(o1PropValue) && this.isObject(o2PropValue) ? this.diff(o1PropValue, o2PropValue, deep) : {newValue: o2PropValue};
          }
        } else {
          removed[prop] = o1PropValue;
        }
      }
    }
    for (const prop in o2) {
      if (o2.hasOwnProperty(prop)) {
        const o1PropValue = o1[prop];
        const o2PropValue = o2[prop];
        if (o1.hasOwnProperty(prop)) {
          if (o1PropValue !== o2PropValue) {
            if (!deep || !this.isObject(o1PropValue)) {
              updated[prop].oldValue = o1PropValue;
            }
          }
        } else {
          added[prop] = o2PropValue;
        }
      }
    }
    return { added, updated, removed, unchanged };
  }

  /**
   * @return if obj is an Object, including an Array.
   */
  static isObject(obj: any) {
    return obj !== null && typeof obj === 'object';
  }
}

Як приклад: дзвінок:

ObjectUtils.diff(
  {
    a: 'a', 
    b: 'b', 
    c: 'c', 
    arr: ['A', 'B'], 
    obj: {p1: 'p1', p2: 'p2'}
  },
  {
    b: 'x', 
    c: 'c', 
    arr: ['B', 'C'], 
    obj: {p2: 'p2', p3: 'p3'}, 
    d: 'd'
  },
);

повернеться:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {oldValue: ['A', 'B'], newValue: ['B', 'C']},
    obj: {oldValue: {p1: 'p1', p2: 'p2'}, newValue: {p2: 'p2', p3: 'p3'}}
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}

і виклик того ж з deepтретім параметром поверне:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {
      added: {},
      removed: {},
      unchanged: {},
      updated: {
        0: {oldValue: 'A', newValue: 'B'},
        1: {oldValue: 'B', newValue: 'C', }
      }
    },
    obj: {
      added: {p3: 'p3'},
      removed: {p1: 'p1'},
      unchanged: {p2: 'p2'},
      updated: {}
    }
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.