无论深度级别如何,如何通过减去属性来获得两个对象的差异?

How to get the difference of two objects by subtracting properties, regardless of depth level?

我想减去两个结构完全相同的对象的值。尽管存在一个答案 ,但它仅限于没有深度的对象。就我而言,我正在寻找一个 robust 解决方案,允许减去 any 深度的对象,只要它们是相同的结构。

例子

考虑以下两个对象,earthData2022earthData2050

const earthData2022 = {
  distanceFromSun: 149280000,
  continents: {
    asia: {
      area: 44579000,
      population: 4560667108,
      countries: { japan: { temperature: 62.5 } },
    },
    africa: { area: 30370000, population: 1275920972 },
    europe: { area: 10180000, population: 746419440 },
    america: { area: 42549000, population: 964920000 },
    australia: { area: 7690000, population: 25925600 },
    antarctica: { area: 14200000, population: 5000 },
  },
};

const earthData2050 = {
  distanceFromSun: 149280000,
  continents: {
    asia: {
      area: 44579000,
      population: 4560767108,
      countries: { japan: { temperature: 73.6 } },
    },
    africa: { area: 30370000, population: 1275960972 },
    europe: { area: 10180000, population: 746419540 },
    america: { area: 42549000, population: 964910000 },
    australia: { area: 7690000, population: 25928600 },
    antarctica: { area: 14200000, population: 5013 },
  },
};

请注意,两个对象都有:

我想减去: earthData2050 - earthData2022 得到一个新对象:

// desired output
// continents' areas aren't expected to change so their diff is `0`
// likewise, the distance of earth from sun
const earthDataDiff = {
  distanceFromSun: 0,
  continents: {
    asia: {
      area: 0,
      population: 100000,
      countries: { japan: { temperature: 11.1 } },
    },
    africa: { area: 0, population: 40000 },
    europe: { area: 0, population: 100 },
    america: { area: 0, population: -10000 },
    australia: { area: 0, population: 3000 },
    antarctica: { area: 0, population: 13 },
  },
};

如上所述,使用给定的甜蜜答案很诱人 :

function mySub(x, y) {
  return Object.keys(x).reduce((a, k) => {
    a[k] = x[k] - y[k];
    return a;
  }, {});
}

然而,当调用 mySub() 时,我们得到了这个不足为奇的输出:

mySub(earthData2050, earthData2022)
// {"distanceFromSun":0,"continents":null}

因此,我的问题是如何递归地减去所有条目,无论多深,前提是对象具有相同的结构。此外,由于我 运行 Node 上的这段代码,我很乐意利用任何可能派上用场的新 ECMAScript 功能。

递归是你的朋友

const earthData2022 = {
  distanceFromSun: 149280000,
  continents: {
    asia: {
      area: 44579000,
      population: 4560667108,
      countries: { japan: { temperature: 62.5 } },
    },
    africa: { area: 30370000, population: 1275920972 },
    europe: { area: 10180000, population: 746419440 },
    america: { area: 42549000, population: 964920000 },
    australia: { area: 7690000, population: 25925600 },
    antarctica: { area: 14200000, population: 5000 },
  },
};

const earthData2050 = {
  distanceFromSun: 149280000,
  continents: {
    asia: {
      area: 44579000,
      population: 4560767108,
      countries: { japan: { temperature: 73.6 } },
    },
    africa: { area: 30370000, population: 1275960972 },
    europe: { area: 10180000, population: 746419540 },
    america: { area: 42549000, population: 964910000 },
    australia: { area: 7690000, population: 25928600 },
    antarctica: { area: 14200000, population: 5013 },
  },
};


function mySub(x, y) {
  const result = {}
  Object.keys(x).forEach((key) => {
    if (typeof x[key] === 'number') {
      result[key] = x[key] - y[key]
    } else {
      result[key] = mySub(x[key], y[key])
    }
  });
  return result;
}

console.log(mySub(earthData2050, earthData2022));

Declarative/functional解法:

const difference = (obj1, obj2) => Object.entries(obj1).reduce((t, [key, value]) => {
    const obj2Value = obj2[key];
    return {
        ...t,
        [key]: typeof value === "object" ?
            difference(value, obj2Value) :
            value - obj2Value
    };
}, {});

说明

Object.entries 将对象转换为 two-dimensional 键值对数组。使用 array.reduce 迭代对,它可以将数组简化为一个对象。与此类比的是,当您在烹饪时将肉汤减少为酱汁时。如果 属性 的值是一个对象,那么得到的 属性 应该是 sub-object 的差值(递归)。如果不是,它必须是一个数字,因此可以减去。

进一步阅读:

这是一个非常简单的递归方法:

const objDiff = (x, y) => 
  Object .fromEntries (Object .entries (x) .map (
    ([k, v]) => [k, Object (v) === v ? objDiff (v, y [k]) : v - y [k]]
  ))

const earthData2022 = {distanceFromSun: 14928e4, continents: {asia: {area: 44579e3, population: 4560667108, countries: {japan: {temperature: 62.5}}}, africa: {area: 3037e4, population: 1275920972}, europe: {area: 1018e4, population: 746419440}, america: {area: 42549e3, population: 96492e4}, australia: {area: 769e4, population: 25925600}, antarctica: {area: 142e5, population: 5e3}}}
const earthData2050 = {distanceFromSun: 14928e4, continents: {asia: {area: 44579e3, population: 4560767108, countries: {japan: {temperature: 73.6}}}, africa: {area: 3037e4, population: 1275960972}, europe: {area: 1018e4, population: 746419540}, america: {area: 42549e3, population: 96491e4}, australia: {area: 769e4, population: 25928600}, antarctica: {area: 142e5, population: 5013}}}

console .log (objDiff (earthData2050, earthData2022))
.as-console-wrapper {max-height: 100% !important; top: 0}

我们根据第一个值是对象还是数字。然后我们使用Object .fromEntries重建一个新的对象。

这只有在你的注释正确的情况下才有效,即两个对象具有相同的结构并且叶节点都是数字。如果我们想处理其他情况,我们必须变得更加复杂。

@Scott 的回答非常高效和优雅。在他的 post 中,他评论 -

This only works if your note is correct, that the two objects have identical structures and that leaf nodes are all numbers. We'd have to get more sophisticated if we wanted to handle other cases.

我想分享那会是什么样子。这里我们写 objDiff 作为 zipMap -

的特化
const objDiff = zipMap((p, q) =>
  is(p, Number) && is(q, Number)
    ? p - q
    : { error: "cannot compute", left: p, right: q }
)

其中 iszipMap 定义为 -

const is = (t, T) => t?.constructor === T

const zipMap = f => (p, q) =>
// Object
  is(p, Object) && is(q, Object)
    ? unique(Object.keys(p), Object.keys(q))
        .reduce((r, k) => Object.assign(r, ({ [k]: zipMap(f)(p[k], q[k]) })), {})
// Array
: is(p, Array) && is(q, Array)
    ? unique(p.keys(), q.keys())
        .map(k => zipMap(f)(p[k], q[k]))
// Else
: f(p, q)

这取决于unique -

const unique = (p, q) =>
  Array.from(new Set([...p, ...q]))

为了演示这一点,我为每个对象添加了一个 sampleArray 属性,并为一个对象添加了一个 hello: "world" 密钥对。 运行 objDiff 下面的代码现在适用于非对称输入和混合值类型 -

const is = (t, T) => t?.constructor === T

const unique = (p, q) =>
  Array.from(new Set([...p, ...q]))

const zipMap = f => (p, q) =>
// Object
  is(p, Object) && is(q, Object)
    ? unique(Object.keys(p), Object.keys(q))
        .reduce((r, k) => Object.assign(r, ({ [k]: zipMap(f)(p[k], q[k]) })), {})
// Array
: is(p, Array) && is(q, Array)
    ? unique(p.keys(), q.keys())
        .map(k => zipMap(f)(p[k], q[k]))
// Else
: f(p, q)

const objDiff = zipMap((p, q) =>
  is(p, Number) && is(q, Number)
    ? p - q
    : { error: "cannot compute", left: p, right: q }
)

const earthData2022 = {sampleArray: [10, 20, 30], distanceFromSun: 14928e4, continents: {asia: {area: 44579e3, population: 4560667108, countries: {japan: {temperature: 62.5}}}, africa: {area: 3037e4, population: 1275920972}, europe: {area: 1018e4, population: 746419440}, america: {area: 42549e3, population: 96492e4}, australia: {area: 769e4, population: 25925600}, antarctica: {area: 142e5, population: 5e3}}}
const earthData2050 = {sampleArray: [9, 40, 30, 100], distanceFromSun: 14928e4, continents: {asia: {area: 44579e3, population: 4560767108, countries: {japan: {temperature: 73.6}}}, africa: {area: 3037e4, population: 1275960972}, europe: {area: 1018e4, population: 746419540}, america: {area: 42549e3, population: 96491e4}, australia: {area: 769e4, population: 25928600}, antarctica: {area: 142e5, population: 5013, hello: "world"}}}

console.log(objDiff(earthData2050, earthData2022))
.as-console-wrapper {max-height: 100% !important; top: 0}

{
  "sampleArray": [
    -1,
    20,
    0,
    {
      "error": "cannot compute",
      "left": 100,
      "right": undefined
    }
  ],
  "distanceFromSun": 0,
  "continents": {
    "asia": {
      "area": 0,
      "population": 100000,
      "countries": {
        "japan": {
          "temperature": 11.099999999999994
        }
      }
    },
    "africa": {
      "area": 0,
      "population": 40000
    },
    "europe": {
      "area": 0,
      "population": 100
    },
    "america": {
      "area": 0,
      "population": -10000
    },
    "australia": {
      "area": 0,
      "population": 3000
    },
    "antarctica": {
      "area": 0,
      "population": 13,
      "hello": {
        "error": "cannot compute",
        "left": "world",
        "right": undefined
      }
    }
  }
}