如何通过减去属性来得到两个对象的差值,而不考虑深度级别?



我想减去具有完全相同结构的两个对象的值。虽然这里存在一个答案,但它仅限于无深度的对象。在我的情况下,我正在寻找一个健壮的解决方案,允许减去任何深度的对象,只要它们具有相同的结构。

考虑以下两个对象,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));

声明式/功能式解决方案:

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将对象转换为键值对的二维数组。使用array.reduce对数组对进行迭代,可以将数组约简为对象。一个类似的例子是当你在烹饪时把肉汤变成酱汁。如果属性的值是一个对象,则生成的属性应该是子对象之差的差(递归)。如果不是,它必须是一个数字,因此可以被减去。

进一步阅读:

  • Array.prototype.reduce
  • 递归(函数调用自身)
  • Object.entries

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

只有当你的笔记是正确的,两个对象具有相同的结构并且叶节点都是数字时,这才有效。如果我们想处理其他情况,我们就得更老练一些。

我想和大家分享一下那是什么样子的。这里我们将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
}
}
}
}

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

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来重建一个新的对象。

只有当你的笔记是正确的,两个对象具有相同的结构并且叶节点都是数字时,这才有效。如果我们想处理其他情况,我们就得更老练一些。

相关内容

  • 没有找到相关文章

最新更新