两个对象之间的通用深度差异

IT技术 javascript object compare
2021-01-28 02:55:02

我有两个对象:oldObjnewObj

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

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

现在我需要不只是从弄清楚什么改变(如添加/更新/删除)oldObjnewObj,却怎么也最能代表它。

到目前为止,我的想法只是构建一个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]]

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

6个回答

我写了一个小类,可以做你想做的,你可以在这里测试

唯一与你的提议不同的是我不考虑

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

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

+1 这不是一段糟糕的代码。但是有一个错误(检查这个例子:jsfiddle.net/kySNu/3 c被创建为undefined但应该是字符串'i am created'),此外它没有做我需要的,因为它缺少深度数组值比较这是最关键(和复杂/困难)的部分。作为旁注,该构造'array' != typeof(obj)是无用的,因为数组是作为数组实例的对象。
2021-03-18 02:55:02
以及您将为该{type: ..., data:..}对象的每个索引获得的数组的“缺乏深度数组值比较”是什么意思缺少的是在第二个数组中搜索第一个数组的值,但正如我在回答中提到的,如果数组的值的顺序不相等([1, 2, 3] is not equal to [3, 2, 1]在我看来),我认为它们不相等
2021-03-24 02:55:02
我更新了代码,但我不确定您想要结果对象的值,现在代码正在从第一个对象返回值,如果它不存在,则第二个对象的值将被设置为数据。
2021-03-30 02:55:02
我同意你的最后一个观点 - 原始数据结构应该更改为更容易进行实际差异的内容。恭喜,你成功了:)
2021-04-01 02:55:02
@MartinJespersen OK,你会如何对待一般这个数组,然后:[{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]现在是用“value1”或“value2”更新的第一个数组中的第一个对象。这是一个简单的例子,深度嵌套可能会变得更加复杂。如果你想/需要深度嵌套而不必考虑关键岗位的不创建对象的数组,创建一个与像前面的例子中嵌套对象的对象:{inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}
2021-04-11 02:55:02

使用下划线,一个简单的差异:

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 在评论中指出的那样,上面只是一个差异 a-->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/以获取完整示例+测试+混合

不知道为什么你被否决了,这已经足够了,因为你提供了一个浅的、简单的例子以及一个更复杂的深层函数。
2021-03-15 02:55:02
@Seiyria 仇恨者会讨厌,我猜...我都做了,因为我最初认为omit这会是一个很大的差异,但错了,所以也包括在内以进行比较。
2021-03-17 02:55:02
不错的解决方案。我建议更改r[k] = ... : vin r[k] = ... : {'a':v, 'b':b[k] },这样您就可以看到两个值。
2021-03-20 02:55:02
它应该_.omitBy代替_.omit.
2021-03-23 02:55:02
当对象在其他方面相同但第二个对象具有更多元素(例如{a:1, b:2}和 )时,这两种方法都返回假阴性{a:1, b:2, c:3}
2021-04-09 02:55:02

我想提供一个 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]
  }
}, {})
请记住,使用此解决方案,对于对象中的每个元素,您都会获得一个全新的对象,其中所有现有元素都复制到其中,只是为了将一项添加到数组中。对于小物体这很好,但对于大物体它会成倍地减慢。
2021-03-15 02:55:02
是的,这不是递归的@Spurious
2021-03-20 02:55:02
代码完整吗?我越来越Uncaught SyntaxError: Unexpected token ...
2021-03-22 02:55:02
不错的解决方案,但您可能想检查该if(o1[key] === o1[key])
2021-03-24 02:55:02
我喜欢这个解决方案,但它有一个问题,如果对象比一个级别更深,它将返回更改后的嵌套对象中的所有值 - 或者至少这对我来说是这样。
2021-03-24 02:55:02

使用 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);
    }
});

我不使用键/对象/源,但如果您需要访问它们,我将其留在那里。对象比较只是防止控制台从最外层元素到最内层元素将差异打印到控制台。

您可以在内部添加一些逻辑来处理数组。也许先对数组进行排序。这是一个非常灵活的解决方案。

编辑

由于 lodash 更新,从 _.merge 更改为 _.mergeWith。感谢 Aviron 注意到这一变化。

在 lodash 4.15.0 _.merge 中不再支持带有定制器功能的,所以你应该使用 _.mergeWith 代替。
2021-03-14 02:55:02
此功能很棒但不适用于嵌套对象。
2021-03-28 02:55:02

这是一个 JavaScript 库,可用于查找两个 JavaScript 对象之间的差异:

Github 网址: https : //github.com/cosmicanant/recursive-diff

npmjs 网址: https ://www.npmjs.com/package/recursive-diff

recursive-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);
例如,这不会考虑日期属性的更改。
2021-03-13 02:55:02
添加日期支持
2021-03-16 02:55:02
看起来这不能识别移动的内容。如正确idenfitying之间的差异 { a: { b: { c: '...' }}},并{ b: { c: '...' }, a: {}}作为“移动/a/b/b
2021-04-04 02:55:02
您可能认为移动是删除然后插入的组合。您可以编写自己的算法来从计算的差异中获取此数据。当您在考虑对象/数组的保留顺序的同时计算 diff 时,恕我直言 diff 算法很简单,否则它可能会更复杂。与数组插入和删除相同的问题,单个插入/删除可以使整个差异数据膨胀。
2021-04-11 02:55:02