我有两个对象:oldObj和newObj。

oldObj中的数据用于填充表单,而newObj是用户更改该表单中的数据并提交的结果。

这两个物体都很深。它们具有对象或对象数组等属性-它们可以有n层深,因此diff算法需要递归。

现在我不仅需要弄清楚从oldObj到newObj更改了什么(如添加/更新/删除),而且还需要知道如何最好地表示它。

到目前为止,我的想法只是构建一个通用的deepdiffbetweenobjects方法,该方法将返回窗体上的对象{add:{…},upd:{…},del:{…但我转念一想:以前一定有人需要它。

所以…有没有人知道一个库或一段代码可以做到这一点,并且可能有更好的方式来表示差异(以一种仍然是JSON可序列化的方式)?

更新:

我想到了一个更好的方法来表示更新的数据,通过使用与newObj相同的对象结构,但将所有属性值转换为窗体上的对象:

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

如果newObj。prop1 = 'new value'和oldObj。prop1 = 'old value'它将设置returnbj。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]]

不仅要检查这种类型的深度值相等相当复杂,而且要找出一种表示可能发生的变化的好方法。


当前回答

这是在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;
        }
    });
}

其他回答

我在这里跌跌撞撞地试图寻找一种方法来区分两个对象之间的区别。这是我使用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

Code snippet below: 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>

这里有一个解决方案:

TypeScript(但很容易转换为JavaScript) 没有lib依赖项 泛型的,不关心检查对象类型(除了对象类型) 支持值为undefined的属性 深度不(默认)

首先,我们定义比较结果接口:

export interface ObjectDiff {
  added: {} | ObjectDiff;
  updated: {
    [propName: string]: Update | ObjectDiff;
  };
  removed: {} | ObjectDiff;
  unchanged: {} | ObjectDiff;
}

对于change的特殊情况,我们想知道什么是旧值和新值:

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

然后我们可以提供只有两个循环的diff函数(如果deep为真,则具有递归性):

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

  /**
   * @param oldObj The previous Object or Array.
   * @param newObj The new Object or Array.
   * @param deep If the comparison must be performed deeper than 1st-level properties.
   * @return A difference summary between the two objects.
   */
  static diff(oldObj: {}, newObj: {}, deep = false): ObjectDiff {
    const added = {};
    const updated = {};
    const removed = {};
    const unchanged = {};
    for (const oldProp in oldObj) {
      if (oldObj.hasOwnProperty(oldProp)) {
        const newPropValue = newObj[oldProp];
        const oldPropValue = oldObj[oldProp];
        if (newObj.hasOwnProperty(oldProp)) {
          if (newPropValue === oldPropValue) {
            unchanged[oldProp] = oldPropValue;
          } else {
            updated[oldProp] = deep && this.isObject(oldPropValue) && this.isObject(newPropValue) ? this.diff(oldPropValue, newPropValue, deep) : {newValue: newPropValue};
          }
        } else {
          removed[oldProp] = oldPropValue;
        }
      }
    }
    for (const newProp in newObj) {
      if (newObj.hasOwnProperty(newProp)) {
        const oldPropValue = oldObj[newProp];
        const newPropValue = newObj[newProp];
        if (oldObj.hasOwnProperty(newProp)) {
          if (oldPropValue !== newPropValue) {
            if (!deep || !this.isObject(oldPropValue)) {
              updated[newProp].oldValue = oldPropValue;
            }
          }
        } else {
          added[newProp] = newPropValue;
        }
      }
    }
    return {added, updated, removed, unchanged};
  }
}

例如,调用:

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 third参数调用同样的方法将返回:

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

2022年更新:

我想出了一个非常简单的算法来解决大多数边缘情况:

使物体变平 简单地比较两个扁平对象并创建一个扁平的diff对象 平展diff对象

如果你保存了被压平的物体,你就可以重复使用它,并在你真正需要的时候进行“反压平…”

let oldObject = {var1:'value1', var2:{ var1:'value1', var2:'value2'},var3:'value3'};
let newObject = {var2:{ var1:'value11', var3:'value3'},var3:'value3'};

let flatOldObject = flattenObject(oldObject)
/*
{
 'var1':'value1',
 'var2.var1':'value1',
 'var2.var2':'value2',
 'var3':'value3' 
}
*/
let flatNewObject = flattenObject(newObject)
/*
{
 'var2.var1':'value11',
 'var2.var3':'value3',
 'var3':'value3'
}
*/
let flatDiff = diffFlatten(flatOldObject, flatNewObject)
let [updated,removed] = flatDiff
/*
updated = {
 'var2.var1':'value11',
 'var2.var3':'value3'
}
removed = {
'var1':'value1'
}
*/

当然,您可以为这些步骤提供您的实现。但我的想法是:

实现

function flattenObject(obj) {
 const object = Object.create(null);
 const path = [];
 const isObject = (value) => Object(value) === value;

 function dig(obj) {
  for (let [key, value] of Object.entries(obj)) {
    path.push(key);
    if (isObject(value)) dig(value);
    else object[path.join('.')] = value;
    path.pop();
  }
 }

 dig(obj);
 return object;
}
function diffFlatten(oldFlat, newFlat) {
    const updated = Object.assign({}, oldFlat);
    const removed = Object.assign({}, newFlat);

    /**delete the unUpdated keys*/
    for (let key in newFlat) {
        if (newFlat[key] === oldFlat[key]) {
             delete updated[key];
             delete removed[key];
        }
    }

    return [updated, removed];

}
function unflatenObject(flattenObject) {
    const unFlatten = Object.create(null);
    for (let [stringKeys, value] of Object.entries(flattenObject)) {
        let chain = stringKeys.split('.')
        let object = unFlatten

        for (let [i, key] of chain.slice(0, -1).entries()) {
            if (!object[key]) {
                let needArray = Number.isInteger(Number(chain[+i + 1]))
                object[key] = needArray ? [] : Object.create(null)
            }
            object = object[key];
        }
        let lastkey = chain.pop();
        object[lastkey] = value;
    }
    return unFlatten;
}
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

有一个每周下载量超过50万的npm模块:https://www.npmjs.com/package/deep-object-diff

我喜欢对象式的差异表示-特别是当它形成时,很容易看到结构。

const diff = require("deep-object-diff").diff;

const lhs = {
  foo: {
    bar: {
      a: ['a', 'b'],
      b: 2,
      c: ['x', 'y'],
      e: 100 // deleted
    }
  },
  buzz: 'world'
};

const rhs = {
  foo: {
    bar: {
      a: ['a'], // index 1 ('b')  deleted
      b: 2, // unchanged
      c: ['x', 'y', 'z'], // 'z' added
      d: 'Hello, world!' // added
    }
  },
  buzz: 'fizz' // updated
};

console.log(diff(lhs, rhs)); // =>
/*
{
  foo: {
    bar: {
      a: {
        '1': undefined
      },
      c: {
        '2': 'z'
      },
      d: 'Hello, world!',
      e: undefined
    }
  },
  buzz: 'fizz'
}
*/

我只是使用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}]