我有两个对象: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]]

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


当前回答

下面是@sbgoran代码的typescript版本

export class deepDiffMapper {

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

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

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

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

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

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

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

    return deepDiffMapper.VALUE_UPDATED;
  }

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

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

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

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

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

      return diff;

  }
}

其他回答

我已经使用这段代码来完成你所描述的任务:

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

这将为您提供一个新对象,该对象将合并窗体中的旧对象和新对象之间的所有更改

下面是一个JavaScript库,可以用来查找两个JavaScript对象之间的差异:

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

npm捷的url:北京方位www.npmjs.犯下/ recursiff

递归-diff库可以在浏览器中使用,也可以在基于Node.js的服务器端应用程序中使用。对于浏览器,它可以使用如下:

<script type="text" src="https://unpkg.com/recursive-diff@latest/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);

下面的方法将创建一个只更改字段的新对象

const findDiff = (obj1, obj2) => {
  const isNativeType1 = typeof obj1 !== "object";
  const isNativeType2 = typeof obj2 !== "object";
  if (isNativeType1 && isNativeType2) {
    return obj1 === obj2 ? null : obj2;
  }
  if (isNativeType1 && !isNativeType2) {
    return obj2;
  }
  if (!isNativeType1 && isNativeType2) {
    return obj2;
  }
  const isArray1 = Array.isArray(obj1);
  const isArray2 = Array.isArray(obj2);
  if (isArray1 && isArray2) {
    const firstLenght = obj1.length;
    const secondLenght = obj2.length;
    const hasSameLength = firstLenght === secondLenght;
    if (!hasSameLength) return obj2;
    let hasChange = false;
    for (let index = 0; index < obj1.length; index += 1) {
      const element1 = obj1[index];
      const element2 = obj2[index];
      const changed = findDiff(element1, element2);
      if (changed) {
        hasChange = true;
      }
    }
    return hasChange ? obj2 : null;
  }
  if (isArray1 || isArray2) return obj2;
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  const hasSameKeys = keys1.length === keys2.length;
  if (!hasSameKeys) {
    const retObj = { ...obj2 };
    for (let index = 0; index < keys1.length; index += 1) {
      const key = keys1[index];
      if (!keys2.includes(key)) {
        retObj[key] = null;
        // eslint-disable-next-line no-continue
        continue;
      }
      delete retObj[key];
    }
    return retObj;
  }
  let hasChange = false;
  const retObj = {};
  for (let index = 0; index < keys1.length; index += 1) {
    const key = keys1[index];
    const element1 = obj1[key];
    const element2 = obj2[key];
    const changed = findDiff(element1, element2);
    if (changed) {
      hasChange = true;
    }
    if (changed) {
      retObj[key] = changed;
    }
  }
  return hasChange ? retObj : null;
};

console.log(
  JSON.stringify(findDiff(
    {
      a: 1,
      b: 2,
      c: {
        a: ['1', 'b', { a: 'b', c: false }, true],
      },
    },
    {
      a: 1,
      b: 2,
      c: {
        a: ['1','b', { a: 'b', c: true }, true],
      },
    }
  ), null, 2)
);

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

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