我有一组营养对象看起来像这样:
[
{
calories: {total: 0, fat: 0},
vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},
fats: {total: 0},
minerals: {calcium: 0}
},
{
calories: {total: 150, fat: 40},
vitamins: {a: {total: 100}, b6: 30, c: 200},
fats: {total: 3}
},
{
calories: {total: 100, fat: 60},
vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},
minerals: {calcium: 20}
}
]
请注意,我添加了一个0填充对象,以便没有属性为null或未定义;并非所有属性都存在于后续对象中;并且这是一个非常深的对象(即obj.vitamins.a.total)
结果应该是
{
calories: {total: 250, fat: 100},
vitamins: {a: {total: 220, retinol: 10}, b6: 30, c: 400},
fats: {total: 3},
minerals: {calcium: 20}
}
我知道lodash
的{{1}}和_.reduce()
函数,但我不确定如何访问_.transform()
上的属性。 如何实现理想的结果,最好使用函数式编程和lodash。
答案 0 :(得分:3)
您可以使用_.mergeWith()
执行此操作。我使用_.spread()
创建了一个可以处理数组而不是单个参数的_.mergeWith()
。
var data = [{"calories":{"total":0,"fat":0},"vitamins":{"a":{"total":0,"retinol":0},"b6":0,"c":0},"fats":{"total":0},"minerals":{"calcium":0}},{"calories":{"total":150,"fat":40},"vitamins":{"a":{"total":100},"b6":30,"c":200},"fats":{"total":3}},{"calories":{"total":100,"fat":60},"vitamins":{"a":{"total":120,"retinol":10},"b6":0,"c":200},"minerals":{"calcium":20}}];
var mergeWith = _.spread(_.mergeWith);
function deepMerge(objs) {
var args = [{}].concat(objs, function(objValue, srcValue) {
if(_.isNumber(objValue)) {
return objValue + srcValue;
}
});
return mergeWith(args);
}
var result = deepMerge(data);
console.log(result);

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
&#13;
使用扩展语法的ES6版本:
var data = [{"calories":{"total":0,"fat":0},"vitamins":{"a":{"total":0,"retinol":0},"b6":0,"c":0},"fats":{"total":0},"minerals":{"calcium":0}},{"calories":{"total":150,"fat":40},"vitamins":{"a":{"total":100},"b6":30,"c":200},"fats":{"total":3}},{"calories":{"total":100,"fat":60},"vitamins":{"a":{"total":120,"retinol":10},"b6":0,"c":200},"minerals":{"calcium":20}}];
function deepMerge(objs) {
return _.mergeWith({}, ...objs, (objValue, srcValue) => {
if (_.isNumber(objValue)) {
return objValue + srcValue;
}
});
}
var result = deepMerge(data);
console.log(result);
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
&#13;
根据@naomik's suggestion,我们还可以将自定义程序提取为外部方法,以便您可以决定非对象值的合并程度。
var data = [{"calories":{"total":0,"fat":0},"vitamins":{"a":{"total":0,"retinol":0},"b6":0,"c":0},"fats":{"total":0},"minerals":{"calcium":0}},{"calories":{"total":150,"fat":40},"vitamins":{"a":{"total":100},"b6":30,"c":200},"fats":{"total":3}},{"calories":{"total":100,"fat":60},"vitamins":{"a":{"total":120,"retinol":10},"b6":0,"c":200},"minerals":{"calcium":20}}];
var mergeWith = _.spread(_.mergeWith);
function customizer(objValue, srcValue) {
if(_.isNumber(objValue)) {
return objValue + srcValue;
}
}
function deepMerge(objs, customizer) {
var args = [{}].concat(objs, function(objValue, srcValue) {
return customizer(objValue, srcValue);
});
return mergeWith(args);
}
var result = deepMerge(data, customizer);
console.log(result);
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
&#13;
答案 1 :(得分:2)
我可以给你一个从Monoids获得灵感的答案。如果没有太过密集的类别理论,你需要两件事来让Monoid工作
Sum Monoid
对于初学者,让我们来看看Sum
Monoid。有许多方法可以在JavaScript中实现这一点,但我们只是看看这个,这样你就可以得到一个粗略的想法
const Sum = x => ({
constructor: Sum,
value: x,
concat: ({value:y}) => Sum(x + y),
fold: f => f (x)
})
Sum.empty = Sum(0)
Sum(3).concat(Sum(4)).fold(x => console.log(x)) // 7
Sum(3).concat(Sum(4)).concat(Sum(9)).fold(x => console.log(x)) // 16
下一级monoid
使用Sum
monoid,我们可以很容易地组合数字,但如果我们的数据更复杂怎么办?例如,如果我们的数据是一个对象怎么办?
想象一下,我们有x
和y
并希望明智地将它们结合起来
let x = SumObject({ a: Sum(1), b: Sum(2) })
let y = SumObject({ b: Sum(3), c: Sum(4) })
x.concat(y)
// => SumObject({ a: Sum(1), b: Sum(5), c: Sum(4) })
那肯定会很棒!让我们现在实现SumObject
幺半群 - 对不起,我真的想不出更好的名字!
const SumObject = x => ({
constructor: SumObject,
value: x,
concat: ({value:y}) => {
return SumObject(Object.keys(y).reduce((acc, k) => {
if (acc[k] === undefined)
return Object.assign(acc, { [k]: y[k] })
else
return Object.assign(acc, { [k]: acc[k].concat(y[k]) })
}, Object.assign({}, x)))
},
fold: f => f(x)
})
SumObject.empty = SumObject({})
圣洁的烟!那个更难一点,但我希望你能看到它。请注意粗体代码。我们对SumObject
的实现假设属性值也是monoid - 意味着值将采用.concat
方法。
让我们成型!
为了让monoids与您的数据一起使用,我们需要将您原始的Object
和Number
类型转换为相应的Monoid类型。
最后,为了在原始类型中获得答案,我们必须将Monoid类型返回转换为原始类型。
如果一切顺利,结果表达式将如下所示
sumToPrimitive(data.map(x =>
primitiveToSum(x)).reduce((acc, x) =>
acc.concat(x), SumObject.empty))
让我们创建将原语转换为Monoid类型的函数
const primitiveToSum = x => {
switch (x.constructor) {
case Object:
return SumObject(Object.keys(x).reduce((acc, k) =>
Object.assign(acc, { [k]: primitiveToSum(x[k]) }), {}))
case Number:
return Sum(x)
default:
throw Error(`unsupported type: ${x}`)
}
}
然后我们将把Monoid转换成原始的
const sumToPrimitive = x => {
switch (x.constructor) {
case SumObject:
return x.fold(x =>
Object.keys(x).reduce((acc, k) =>
Object.assign(acc, { [k]: sumToPrimitive(x[k]) }), {}))
case Sum:
return x.fold(identity)
default:
throw Error(`unsupported type: ${x}`)
}
}
“那就是它?”
好吧,因为你支持嵌套的对象,它有点乱,但这真的是我们能做的最好的,我很害怕。如果你愿意将数据限制为扁平物体,事情会更加清晰。
尽管如此,让我们看看它的全部工作
const SumObject = x => ({
constructor: SumObject,
value: x,
concat: ({value:y}) => {
return SumObject(Object.keys(y).reduce((acc, k) => {
if (acc[k] === undefined)
return Object.assign(acc, { [k]: y[k] })
else
return Object.assign(acc, { [k]: acc[k].concat(y[k]) })
}, Object.assign({}, x)))
},
fold: f => f(x)
})
SumObject.empty = SumObject({})
const Sum = x => ({
constructor: Sum,
value: x,
concat: ({value:y}) => Sum(x + y),
fold: f => f (x)
})
Sum.empty = Sum(0)
const primitiveToSum = x => {
switch (x.constructor) {
case Object:
return SumObject(Object.keys(x).reduce((acc, k) =>
Object.assign(acc, { [k]: primitiveToSum(x[k]) }), {}))
case Number:
return Sum(x)
default:
throw Error(`unsupported type: ${x}`)
}
}
const sumToPrimitive = x => {
switch (x.constructor) {
case SumObject:
return x.fold(x =>
Object.keys(x).reduce((acc, k) =>
Object.assign(acc, { [k]: sumToPrimitive(x[k]) }), {}))
case Sum:
return x.fold(identity)
default:
throw Error(`unsupported type: ${x}`)
}
}
const identity = x => x
const data = [
{
calories: {total: 0, fat: 0},
vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},
fats: {total: 0},
minerals: {calcium: 0}
},
{
calories: {total: 150, fat: 40},
vitamins: {a: {total: 100}, b6: 30, c: 200},
fats: {total: 3}
},
{
calories: {total: 100, fat: 60},
vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},
minerals: {calcium: 20}
}
]
const result = sumToPrimitive(data.map(x =>
primitiveToSum(x)).reduce((acc, x) =>
acc.concat(x), SumObject.empty))
console.log(result)
<强>输出强>
{ calories: { total: 250, fat: 100 },
vitamins: { a: { total: 220, retinol: 10 }, b6: 30, c: 400 },
fats: { total: 3 },
minerals: { calcium: 20 } }
<强>说明强>
Monoids是一个很好的界面,用于维护泛型,可重用的方式来组合术语 - 这就是函数式编程的全部内容。但是,由于您的数据嵌套复杂(以及本机JavaScript缺少自定义类型),因此需要更多手动编码。
我无法想象Lodash解决方案会是什么样子 - 但我可以告诉你的是,它会将泛型和可重用性抛到窗外 - 特别是考虑到数据的结构。
答案 2 :(得分:2)
尽管Ori Drori's answer已经指定了使用mergeWith的方向,但他的用法在于创建递归调用mergeWith
的递归函数。此解决方案忽略了mergeWith
本质上是递归合并的事实,其中定制器仅在返回任何不是undefined
的值时才转换结果值。另外,另一个问题是mergeWith
会改变第一个对象参数,这在你想要保留源对象状态的情况下可能并不理想。
下面的解决方案使用Function.prototype.apply与mergeWith
而不是将其包裹在spread中,您可以根据自己的喜好决定将其切换为spread
。为了解决源对象变异的问题,我们提供一个空对象作为第一个参数,然后将其与其余的源对象连接起来。最后,我们连接定制器函数,如果value
是数字,则返回参数的总和。
var result = _.mergeWith.apply(
null,
[{}].concat(source).concat(function(value, src) {
if(_.isNumber(value)) {
return value + src;
}
})
);
var source = [
{
calories: {total: 0, fat: 0},
vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},
fats: {total: 0},
minerals: {calcium: 0}
},
{
calories: {total: 150, fat: 40},
vitamins: {a: {total: 100}, b6: 30, c: 200},
fats: {total: 3}
},
{
calories: {total: 100, fat: 60},
vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},
minerals: {calcium: 20}
}
];
var result = _.mergeWith.apply(
null,
[{}].concat(source).concat(function(value, src) {
if(_.isNumber(value)) {
return value + src;
}
})
);
console.log(result);
body>div {
min-height: 100%;
top: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
答案 3 :(得分:0)
我更喜欢以无点的方式与Ramda一起做这件事,但OP在Lodash要求,所以我们在这里,简单的递归解决方案:
let data = [{calories: {total: 0, fat: 0},vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},fats: {total: 0},minerals: {calcium: 0}},{calories: {total: 150, fat: 40},vitamins: {a: {total: 100}, b6: 30, c: 200},fats: {total: 3}},{calories: {total: 100, fat: 60},vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},minerals: {calcium: 20}}];
const recursor = result => (value, key) => {
if ( _.isNumber(value) ) {
result[key] = ((result[key] || (result[key] = 0)) + value);
return result[key];
} else {
return _.mapValues(value, recursor(result[key] || (result[key] = {}) ));
}
}
const reducer = (result, value, key) => _.mapValues(value, recursor(result));
let final = _.transform(data, reducer, {});
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>