关于javascript:如何使用lodash对2个对象进行深入比较?

How to do a deep comparison between 2 objects with lodash?

我有两个不同的嵌套对象,我需要知道它们在其中一个嵌套属性中是否存在差异。

1
2
3
4
5
6
7
8
var a = {};
var b = {};

a.prop1 = 2;
a.prop2 = { prop3: 2 };

b.prop1 = 2;
b.prop2 = { prop3: 3 };

对象可能更复杂,具有更多的嵌套属性。但这是一个很好的例子。我可以选择使用递归函数或与lodash一起使用…


一个简单而优雅的解决方案是使用_.isEqual,它进行了深入的比较:

1
2
3
4
5
6
7
8
9
10
var a = {};
var b = {};

a.prop1 = 2;
a.prop2 = { prop3: 2 };

b.prop1 = 2;
b.prop2 = { prop3: 3 };

_.isEqual(a, b); // returns false if different

但是,此解决方案不显示哪个属性不同。

http://jsfiddle.net/bdkeyn0h/


如果需要知道哪些属性不同,请使用reduce():

1
2
3
4
5
_.reduce(a, function(result, value, key) {
    return _.isEqual(value, b[key]) ?
        result : result.concat(key);
}, []);
// → ["prop2" ]


对于任何在这条线上绊倒的人,这里有一个更完整的解决方案。它将比较两个对象,并为您提供所有属性的键,这些属性要么仅在对象1中,要么仅在对象2中,要么都在对象1和对象2中,但具有不同的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
 * Compare two objects by reducing an array of keys in obj1, having the
 * keys in obj2 as the intial value of the result. Key points:
 *
 * - All keys of obj2 are initially in the result.
 *
 * - If the loop finds a key (from obj1, remember) not in obj2, it adds
 *   it to the result.
 *
 * - If the loop finds a key that are both in obj1 and obj2, it compares
 *   the value. If it's the same value, the key is removed from the result.
 */

function getObjectDiff(obj1, obj2) {
    const diff = Object.keys(obj1).reduce((result, key) => {
        if (!obj2.hasOwnProperty(key)) {
            result.push(key);
        } else if (_.isEqual(obj1[key], obj2[key])) {
            const resultKeyIndex = result.indexOf(key);
            result.splice(resultKeyIndex, 1);
        }
        return result;
    }, Object.keys(obj2));

    return diff;
}

下面是一个输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Test
let obj1 = {
    a: 1,
    b: 2,
    c: { foo: 1, bar: 2},
    d: { baz: 1, bat: 2 }
}

let obj2 = {
    b: 2,
    c: { foo: 1, bar: 'monkey'},
    d: { baz: 1, bat: 2 }
    e: 1
}
getObjectDiff(obj1, obj2)
// ["c","e","a"]

如果您不关心嵌套对象,并且希望跳过lodash,可以用_.isEqual代替正常值比较,例如obj1[key] === obj2[key]


基于AdamBoduch的回答,我编写了这个函数,它从最深层的意义上比较两个对象,返回具有不同值的路径以及一个或另一个对象缺少的路径。

代码的编写并没有考虑到效率问题,在这方面的改进是最受欢迎的,但下面是基本形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var compare = function (a, b) {

  var result = {
    different: [],
    missing_from_first: [],
    missing_from_second: []
  };

  _.reduce(a, function (result, value, key) {
    if (b.hasOwnProperty(key)) {
      if (_.isEqual(value, b[key])) {
        return result;
      } else {
        if (typeof (a[key]) != typeof ({}) || typeof (b[key]) != typeof ({})) {
          //dead end.
          result.different.push(key);
          return result;
        } else {
          var deeper = compare(a[key], b[key]);
          result.different = result.different.concat(_.map(deeper.different, (sub_path) => {
            return key +"." + sub_path;
          }));

          result.missing_from_second = result.missing_from_second.concat(_.map(deeper.missing_from_second, (sub_path) => {
            return key +"." + sub_path;
          }));

          result.missing_from_first = result.missing_from_first.concat(_.map(deeper.missing_from_first, (sub_path) => {
            return key +"." + sub_path;
          }));
          return result;
        }
      }
    } else {
      result.missing_from_second.push(key);
      return result;
    }
  }, result);

  _.reduce(b, function (result, value, key) {
    if (a.hasOwnProperty(key)) {
      return result;
    } else {
      result.missing_from_first.push(key);
      return result;
    }
  }, result);

  return result;
}

您可以使用此代码段尝试代码(建议以整页模式运行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
var compare = function (a, b) {

  var result = {
    different: [],
    missing_from_first: [],
    missing_from_second: []
  };

  _.reduce(a, function (result, value, key) {
    if (b.hasOwnProperty(key)) {
      if (_.isEqual(value, b[key])) {
        return result;
      } else {
        if (typeof (a[key]) != typeof ({}) || typeof (b[key]) != typeof ({})) {
          //dead end.
          result.different.push(key);
          return result;
        } else {
          var deeper = compare(a[key], b[key]);
          result.different = result.different.concat(_.map(deeper.different, (sub_path) => {
            return key +"." + sub_path;
          }));

          result.missing_from_second = result.missing_from_second.concat(_.map(deeper.missing_from_second, (sub_path) => {
            return key +"." + sub_path;
          }));

          result.missing_from_first = result.missing_from_first.concat(_.map(deeper.missing_from_first, (sub_path) => {
            return key +"." + sub_path;
          }));
          return result;
        }
      }
    } else {
      result.missing_from_second.push(key);
      return result;
    }
  }, result);

  _.reduce(b, function (result, value, key) {
    if (a.hasOwnProperty(key)) {
      return result;
    } else {
      result.missing_from_first.push(key);
      return result;
    }
  }, result);

  return result;
}

var a_editor = new JSONEditor($('#a')[0], {
  name: 'a',
  mode: 'code'
});
var b_editor = new JSONEditor($('#b')[0], {
  name: 'b',
  mode: 'code'
});

var a = {
  same: 1,
  different: 2,
  missing_from_b: 3,
  missing_nested_from_b: {
    x: 1,
    y: 2
  },
  nested: {
    same: 1,
    different: 2,
    missing_from_b: 3
  }
}

var b = {
  same: 1,
  different: 99,
  missing_from_a: 3,
  missing_nested_from_a: {
    x: 1,
    y: 2
  },
  nested: {
    same: 1,
    different: 99,
    missing_from_a: 3
  }
}

a_editor.set(a);
b_editor.set(b);

var result_editor = new JSONEditor($('#result')[0], {
  name: 'result',
  mode: 'view'
});

var do_compare = function() {
  var a = a_editor.get();
  var b = b_editor.get();
  result_editor.set(compare(a, b));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#objects {} #objects section {
  margin-bottom: 10px;
}
#objects section h1 {
  background: #444;
  color: white;
  font-family: monospace;
  display: inline-block;
  margin: 0;
  padding: 5px;
}
.jsoneditor-outer, .ace_editor {
min-height: 230px !important;
}
button:hover {
  background: orangered;
}
button {
  cursor: pointer;
  background: red;
  color: white;
  text-align: left;
  font-weight: bold;
  border: 5px solid crimson;
  outline: 0;
  padding: 10px;
  margin: 10px 0px;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<link href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.5.10/jsoneditor.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.5.10/jsoneditor.min.js">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">

  <section>
    a (first object)
   
  </section>
  <section>
    b (second object)
   
  </section>
  <button onClick="do_compare()">compare</button>
  <section>
    result
   
  </section>


下面是一个简明的解决方案:

1
_.differenceWith(a, b, _.isEqual);


要递归地显示一个对象与另一个对象的不同之处,您可以使用uureduce与u.isequal和.isplainobject组合使用。在这种情况下,您可以比较A与B的区别或B与A的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = {prop1: {prop1_1: 'text 1', prop1_2: 'text 2', prop1_3: [1, 2, 3]}, prop2: 2, prop3: 3};
var b = {prop1: {prop1_1: 'text 1', prop1_3: [1, 2]}, prop2: 2, prop3: 4};

var diff = function(obj1, obj2) {
  return _.reduce(obj1, function(result, value, key) {
    if (_.isPlainObject(value)) {
      result[key] = diff(value, obj2[key]);
    } else if (!_.isEqual(value, obj2[key])) {
      result[key] = value;
    }
    return result;
  }, {});
};

var res1 = diff(a, b);
var res2 = diff(b, a);
console.log(res1);
console.log(res2);
1
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js">


此代码返回一个对象,该对象具有所有具有不同值的属性以及两个对象的值。有助于记录差异。

1
2
3
4
5
6
7
var allkeys = _.union(_.keys(obj1), _.keys(obj2));
var difference = _.reduce(allkeys, function (result, key) {
  if ( !_.isEqual(obj1[key], obj2[key]) ) {
    result[key] = {obj1: obj1[key], obj2: obj2[key]}
  }
  return result;
}, {});

使用(嵌套)属性模板进行深度比较以检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function objetcsDeepEqualByTemplate(objectA, objectB, comparisonTemplate) {
  if (!objectA || !objectB) return false

  let areDifferent = false
  Object.keys(comparisonTemplate).some((key) => {
    if (typeof comparisonTemplate[key] === 'object') {
      areDifferent = !objetcsDeepEqualByTemplate(objectA[key], objectB[key], comparisonTemplate[key])
      return areDifferent
    } else if (comparisonTemplate[key] === true) {
      areDifferent = objectA[key] !== objectB[key]
      return areDifferent
    } else {
      return false
    }
  })

  return !areDifferent
}

const objA = {
  a: 1,
  b: {
    a: 21,
    b: 22,
  },
  c: 3,
}

const objB = {
  a: 1,
  b: {
    a: 21,
    b: 25,
  },
  c: true,
}

// template tells which props to compare
const comparisonTemplateA = {
  a: true,
  b: {
    a: true
  }
}
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateA)
// returns true

const comparisonTemplateB = {
  a: true,
  c: true
}
// returns false
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateB)

这将在控制台中工作。如果需要,可以添加数组支持


如果不使用lodash/underline,我已经编写了这段代码,并且可以很好地将object1与object2进行深入比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function getObjectDiff(a, b) {
    var diffObj = {};
    if (Array.isArray(a)) {
        a.forEach(function(elem, index) {
            if (!Array.isArray(diffObj)) {
                diffObj = [];
            }
            diffObj[index] = getObjectDiff(elem, (b || [])[index]);
        });
    } else if (a != null && typeof a == 'object') {
        Object.keys(a).forEach(function(key) {
            if (Array.isArray(a[key])) {
                var arr = getObjectDiff(a[key], b[key]);
                if (!Array.isArray(arr)) {
                    arr = [];
                }
                arr.forEach(function(elem, index) {
                    if (!Array.isArray(diffObj[key])) {
                        diffObj[key] = [];
                    }
                    diffObj[key][index] = elem;
                });
            } else if (typeof a[key] == 'object') {
                diffObj[key] = getObjectDiff(a[key], b[key]);
            } else if (a[key] != (b || {})[key]) {
                diffObj[key] = a[key];
            } else if (a[key] == (b || {})[key]) {
                delete a[key];
            }
        });
    }
    Object.keys(diffObj).forEach(function(key) {
        if (typeof diffObj[key] == 'object' && JSON.stringify(diffObj[key]) == '{}') {
            delete diffObj[key];
        }
    });
    return diffObj;
}

我刺穿了Adam Boduch的代码以输出深度差异-这完全是未经测试的,但片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function diff (obj1, obj2, path) {
    obj1 = obj1 || {};
    obj2 = obj2 || {};

    return _.reduce(obj1, function(result, value, key) {
        var p = path ? path + '.' + key : key;
        if (_.isObject(value)) {
            var d = diff(value, obj2[key], p);
            return d.length ? result.concat(d) : result;
        }
        return _.isEqual(value, obj2[key]) ? result : result.concat(p);
    }, []);
}

diff({ foo: 'lol', bar: { baz: true }}, {}) // returns ["foo","bar.baz"]


如果只需要关键比较:

1
2
3
 _.reduce(a, function(result, value, key) {
     return b[key] === undefined ? key : []
  }, []);


完成亚当·博杜克的回答后,这个问题考虑到了属性的差异。

1
2
3
4
5
6
const differenceOfKeys = (...objects) =>
  _.difference(...objects.map(obj => Object.keys(obj)));
const differenceObj = (a, b) =>
  _.reduce(a, (result, value, key) => (
    _.isEqual(value, b[key]) ? result : [...result, key]
  ), differenceOfKeys(b, a));

正如所要求的,这里有一个递归的对象比较函数。还有一点。假设这种功能的主要用途是对象检查,我有话要说。当某些差异无关紧要时,完全深入比较是一个坏主意。例如,TDD断言中的盲深度比较会使测试变得不必要的脆弱。出于这个原因,我想介绍一个更有价值的部分diff,它是对这个线程先前贡献的递归模拟。它忽略不存在于

1
2
3
4
5
6
var bdiff = (a, b) =>
    _.reduce(a, (res, val, key) =>
        res.concat((_.isPlainObject(val) || _.isArray(val)) && b
            ? bdiff(val, b[key]).map(x => key + '.' + x)
            : (!b || val != b[key] ? [key] : [])),
        []);

bdiff允许在容忍其他属性的同时检查期望值,这正是您想要的自动检查。这允许构建各种高级断言。例如:

1
2
3
4
5
var diff = bdiff(expected, actual);
// all expected properties match
console.assert(diff.length == 0,"Objects differ", diff, expected, actual);
// controlled inequality
console.assert(diff.length < 3,"Too many differences", diff, expected, actual);

返回完整的解决方案。使用bdiff构建完整的传统diff非常简单:

1
2
3
4
5
6
function diff(a, b) {
    var u = bdiff(a, b), v = bdiff(b, a);
    return u.filter(x=>!v.includes(x)).map(x=>' < ' + x)
    .concat(u.filter(x=>v.includes(x)).map(x=>' | ' + x))
    .concat(v.filter(x=>!u.includes(x)).map(x=>' > ' + x));
};

在两个复杂对象上运行上述函数将输出类似于以下内容的内容:

1
2
3
4
5
6
7
8
9
 [
 " < components.0.components.1.components.1.isNew",
 " < components.0.cryptoKey",
 " | components.0.components.2.components.2.components.2.FFT.min",
 " | components.0.components.2.components.2.components.2.FFT.max",
 "> components.0.components.1.components.1.merkleTree",
 "> components.0.components.2.components.2.components.2.merkleTree",
 "> components.0.components.3.FFTResult"
 ]

最后,为了了解值之间的差异,我们可能需要直接对diff输出进行eval()。为此,我们需要一个更糟糕的bdiff版本,它输出语法正确的路径:

1
2
3
4
5
6
7
8
9
10
11
// provides syntactically correct output
var bdiff = (a, b) =>
    _.reduce(a, (res, val, key) =>
        res.concat((_.isPlainObject(val) || _.isArray(val)) && b
            ? bdiff(val, b[key]).map(x =>
                key + (key.trim ? '':']') + (x.search(/^\d/)? '.':'[') + x)
            : (!b || val != b[key] ? [key + (key.trim ? '':']')] : [])),
        []);

// now we can eval output of the diff fuction that we left unchanged
diff(a, b).filter(x=>x[1] == '|').map(x=>[x].concat([a, b].map(y=>((z) =>eval('z.' + x.substr(3))).call(this, y)))));

它将输出类似于以下内容的内容:

1
2
[" | components[0].components[2].components[2].components[2].FFT.min", 0, 3]
[" | components[0].components[2].components[2].components[2].FFT.max", 100, 50]

麻省理工学院执照;


下面是一个简单的带有lodash深度差分检查器的字体脚本,它将生成一个新的对象,只是旧对象和新对象之间的差异。

例如,如果我们有:

1
2
const oldData = {a: 1, b: 2};
const newData = {a: 1, b: 3};

结果对象为:

1
const result: {b: 3};

它还与多层深度对象兼容,对于数组,可能需要进行一些调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as _ from"lodash";

export const objectDeepDiff = (data: object | any, oldData: object | any) => {
  const record: any = {};
  Object.keys(data).forEach((key: string) => {
    // Checks that isn't an object and isn't equal
    if (!(typeof data[key] ==="object" && _.isEqual(data[key], oldData[key]))) {
      record[key] = data[key];
    }
    // If is an object, and the object isn't equal
    if ((typeof data[key] ==="object" && !_.isEqual(data[key], oldData[key]))) {
      record[key] = objectDeepDiff(data[key], oldData[key]);
    }
  });
  return record;
};

1
2
3
4
5
6
7
8
9
10
11
var isEqual = function(f,s) {
  if (f === s) return true;

  if (Array.isArray(f)&&Array.isArray(s)) {
    return isEqual(f.sort(), s.sort());
  }
  if (_.isObject(f)) {
    return isEqual(f, s);
  }
  return _.isEqual(f, s);
};


这是基于@jlavoie,使用lodash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let differences = function (newObj, oldObj) {
      return _.reduce(newObj, function (result, value, key) {
        if (!_.isEqual(value, oldObj[key])) {
          if (_.isArray(value)) {
            result[key] = []
            _.forEach(value, function (innerObjFrom1, index) {
              if (_.isNil(oldObj[key][index])) {
                result[key].push(innerObjFrom1)
              } else {
                let changes = differences(innerObjFrom1, oldObj[key][index])
                if (!_.isEmpty(changes)) {
                  result[key].push(changes)
                }
              }
            })
          } else if (_.isObject(value)) {
            result[key] = differences(value, oldObj[key])
          } else {
            result[key] = value
          }
        }
        return result
      }, {})
    }

https://jsfidle.net/emilianobarza/0g0sn3b9/8/