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

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


当前回答

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

其他回答

从sbgoran的答案中得到扩展和简化的函数。 这允许深度扫描和发现数组的相似性。

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

这将把[1,2,3]和[3,2,1]视为相等(deep object) 因为我需要把它们之间的区别形象化:

[
  {
    "a":1,
    "b":1
  },
  {
    "a":1,
    "b":1
  }
]

and

[
  {
    "a":1,
    "b":1
  },
  {
    "a":"OH NO",
    "b":"an insertion"
  },
  {
    "a":1,
    "b":1
  }
]

所以我想看看它们的碰撞,这是剩下的:

[]
and
[
  {
    "a":"OH NO",
    "b":"an insertion"
  }
]

在我看来,这是最好的表现方式。 {添加:{…},乌利希期刊指南:{…},德尔:{…}}很难读懂


我提供了2个函数:objectcollision (obj1,obj2)和arraycollision (arr1,arr2)

console.log(ArrayCollide([1,2,3],[3,2,1]))
// false
//everything collided -> false
console.log(ArrayCollide([1],[2,1]))
// [ [], [ 2 ] ]
//1 and 1 collided, even if they are on different indices

//array of objects
const arr1 = 
[
  {
    "a":1,
    "b":1
  },
  {
    "a":1,
    "b":1
  }
]
const arr2 = 
[
  {
    "a":1,
    "b":1
  },
  {
    "a":"OH NO",
    "b":"an insertion"
  },
  {
    "a":1,
    "b":1
  }
]
const newArrays = ArrayCollide(arr1, arr2)
console.log(newArrays[0])
console.log(newArrays[1])
console.log('\n')
// []
// [ { a: 'OH NO', b: 'an insertion' } ]
// everything collided until this is left

//ObjectCollide
const obj1 = { a: '111', c: { q: 'no', a: '333' } }
const obj2 = { a: '111', p: 'ok', c: { a: '333' } }
ObjectCollide(obj1, obj2) //in place
console.log(obj1)
console.log(obj2)
console.log('\n')
// { c: { q: 'no' } }
// { p: 'ok', c: {} }
// obj["a"] collided and obj["c"]["a"] collided

//testing empty array
const a1 = { a: [] }
const a2 = { a: [], b: '2' }
ObjectCollide(a1, a2) //in place
console.log(a1)
console.log(a2)
console.log('\n')
// {}
// { b: '2' }
// obj["a"] collided

//DIFFERENT TYPES
const b1 = {a:true}
const b2 = {a:[1,2]}
ObjectCollide(b1,b2) //in place
console.log(b1)
console.log(b2)
// { a: true }
// { a: [ 1, 2 ] }

function ObjectCollide(obj1, obj2) {
  //in place, returns true if same

  // delete same
  const keys = Object.keys(obj1)
  const len = keys.length
  let howManyDeleted = 0
  for (let i = 0; i < len; i++) {
    const key = keys[i]

    const type1 = Array.isArray(obj1[key]) === true ? 'array' : typeof obj1[key]
    const type2 = Array.isArray(obj2[key]) === true ? 'array' : typeof obj2[key]
    if (type1!==type2) {
      continue
    }
    switch (type1) {
      case 'object':
        if (ObjectCollide(obj1[key], obj2[key])) {
          delete obj1[key]
          delete obj2[key]
          howManyDeleted++
        }
        continue
      case 'array':
        const newArrays = ArrayCollide(obj1[key], obj2[key])
        if (newArrays) {
          obj1[key] = newArrays[0]
          obj2[key] = newArrays[1]
        } else {
          delete obj1[key]
          delete obj2[key]
          howManyDeleted++
        }
        continue
      default:
        //string, number, I hope it covers everything else
        if (obj1[key] === obj2[key]) {
          delete obj1[key]
          delete obj2[key]
          howManyDeleted++
        }
    }
  }


  if (howManyDeleted === len && Object.keys(obj2).length === 0) {
    // return 'delete the stuff'
    // same. we've deleted everything!
    return true
  }

}
function ArrayCollide(arr1, arr2) {
  // returns [newArr1, newArr2] or false if same arrays (ignore order)
  const stringifyObj = {}

  const newArr1 = []
  const newArr2 = []
  for (let i = 0, len = arr1.length; i < len; i++) {
    const value = arr1[i]
    const stringified = JSON.stringify(value)
    stringifyObj[stringified]
    // arr = [count, ...]
    const arr = stringifyObj[stringified] || (stringifyObj[stringified] = [0])
    arr[0]++
    arr.push(value)
  }
  //in 2 but not in 1
  for (let i = 0, len = arr2.length; i < len; i++) {
    const value = arr2[i]
    const stringified = JSON.stringify(value)
    const arr = stringifyObj[stringified]
    if (arr === undefined) {
      newArr2.push(value)
    } else {
      if (arr[0] === 0) {
        newArr2.push(value)
      } else {
        arr[0]--
      }
    }
  }
  //in 1 but not in 2
  stringifyKeys = Object.keys(stringifyObj)
  for (let i = 0, len = stringifyKeys.length; i < len; i++) {
    const arr = stringifyObj[stringifyKeys[i]]

    for (let i = 1, len = arr[0] + 1; i < len; i++) {
      newArr1.push(arr[i])
    }
  }
  if (newArr1.length || newArr2.length) {
    return [newArr1, newArr2]
  } else {
    return false
  }

}

我想解决的问题是:

JSON文件不断重新排序,我想恢复JSON,如果它是等效的:像{a:1,b:2}和{b:2,a:1} 但是因为我不相信我的代码(我犯了一个错误),我想看到diff并自己检查它,我可以Ctrl+F到原始文件使用这个diff。

我知道我迟到了,但我需要类似的东西,上面的答案没有帮助。

我使用了Angular的$watch函数来检测变量的变化。我不仅需要知道变量上的属性是否发生了变化,而且还需要确保发生变化的属性不是一个临时的计算字段。换句话说,我想忽略某些属性。

代码如下:

function diff(obj1,obj2,exclude) {
        var r = {};
    
    if (!exclude)   exclude = [];
    
    for (var prop in obj1) {
      if (obj1.hasOwnProperty(prop) && prop != '__proto__') {
            if (exclude.indexOf(obj1[prop]) == -1) {

            // check if obj2 has prop
            if (!obj2.hasOwnProperty(prop)) r[prop] = obj1[prop];

            // check if prop is object and 
            // NOT a JavaScript engine object (i.e. __proto__), if so, recursive diff
            else if (obj1[prop] === Object(obj1[prop])) {
              var difference = diff(obj1[prop], obj2[prop]);
              if (Object.keys(difference).length > 0) r[prop] = difference;
            }

            // check if obj1 and obj2 are equal
            else if (obj1[prop] !== obj2[prop]) {
                if (obj1[prop] === undefined)
                r[prop] = 'undefined';
                if (obj1[prop] === null)
                r[prop] = null;
              else if (typeof obj1[prop] === 'function')
                r[prop] = 'function';
              else if (typeof obj1[prop] === 'object')  
                r[prop] = 'object';
              else
                  r[prop] = obj1[prop];
            }
          }
       }
       
    }
    
    return r;
}

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

希望这能帮助到一些人。

如今,有相当多的模块可用于此。我最近写了一个模块来做到这一点,因为我不满意我发现的大量不同的模块。它被称为odiff: https://github.com/Tixit/odiff。我还列出了一些最流行的模块,以及为什么它们不能在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}]}
]
*/

我修改了@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);
        }
    }
}();