我想创建将数组与特定键分组的函数,如下所示:
var items = [
{name: 'n1', prop: 'p1', value: 90},
{name: 'b', prop: 'p2', value: 1},
{name: 'n1', prop: 'p3', value: 3}];
对此:
{n1: {p1: 90, p3: 3}, {b: {p2: 1}
基本上按“名称”列分组,并将属性名称设置为具有值的键。
我知道RamdaJs中有groupBy函数,但是它接受函数来生成组密钥。
我知道之后可以格式化数据,但是效率不高。
有什么方法可以传递某种“转换”功能来为每一项准备数据。
谢谢
答案 0 :(得分:3)
在使用通用库并为每种情况编写自定义代码时需要进行权衡。像Ramda这样的具有数百个功能的库将提供许多可以提供帮助的工具,但它们不可能涵盖所有情况。 Ramda确实具有将groupBy
与某种折叠方式reduceBy
结合使用的特定功能。但是如果我不知道,我会写一个自定义版本。
我将从有效且简单的方法开始,仅在测试显示此特定代码存在问题时担心性能。在这里,我展示了每次更改此类功能以提高性能的许多步骤。我在这里要说明要点:实际上,我会坚持使用我的第一个版本,该版本我很容易阅读,并且不会为任何性能增强而烦恼,除非我有确切的数字表明这是我的应用程序中的瓶颈。 / p>
我的第一遍可能看起来像这样:
const addTo = (obj, {prop, value}) =>
assoc (prop, value, obj)
const transform1 = pipe (
groupBy (prop ('name')),
map (reduce (addTo, {}))
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform1 (items)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {assoc, pipe, groupBy, prop, map, reduce} = R </script>
对我来说,这很清楚而且很容易理解。
但是肯定存在效率问题,因为我们必须遍历该列表进行分组,然后遍历每个组进行折叠。因此,也许我们最好使用自定义函数。这是一个相当简单的现代JS版本:
const transform2 = (items) =>
items .reduce(
(a, {name, prop, value}) => ({...a, [name]: {...a[name], [prop]: value}}),
{}
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform2 (items)
)
reduce ({...spread})
此版本仅循环一次,这听起来是一个不错的改进……但是对于Rich Snap称为reduce ({...spread}) anti-pattern的性能存在真正的疑问。所以也许我们想使用变异减少。这不应该引起问题,因为它只是我们功能的内部。我们可以编写不涉及此reduce ({...spread}) pattern
的等效版本:
const transform3 = (items) =>
items .reduce (
(a, {name, prop, value}) => {
const obj = a [name] || {}
obj[prop] = value
a[name] = obj
return a
},
{}
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform3 (items)
)
现在,我们已经删除了该模式(我实际上并不同意它总是一个反模式),我们有一些性能更高的代码,但是我们仍然可以做一件事。众所周知,Array.prototype
之类的功能reduce
并不比它们的普通循环功能快。因此,我们可以更进一步,并使用for
循环编写此代码:
const transform4 = (items) => {
const res = {}
for (let i = 0; i < items .length; i++) {
const {name, prop, value} = items [i]
const obj = res [name] || {}
obj[prop] = value
}
return res
}
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]; console.log('This version is intentionally broken. See the text for the fix.');
console .log (
transform4 (items)
)
在性能优化方面,我们已经达到了我能想到的极限。
...而且我们使代码更糟!将最后一个版本与第一个版本
进行比较const transform1 = pipe (
groupBy (prop ('name')),
map (reduce (addTo, {}))
)
在代码清晰性方面,我们看到了毫无疑问的赢家。在不了解addTo
助手的详细信息的情况下,我们仍然可以预先了解该函数在初读时的作用。如果我们希望这些细节更加明显,我们可以简单地内联该帮助程序。不过,版本将需要仔细阅读以了解其工作原理。
哦,等等;它不起作用。您是否测试过并看到了?你看到什么丢失了吗?我从for
循环的结尾拉出了这一行:
res[name] = obj;
您在代码中注意到了吗?发现它并不是特别困难,但是乍一看并不一定很明显。
性能优化在需要时必须非常小心地完成,因为您无法利用许多习惯使用的工具。因此,有时候它非常重要,然后我就去做,但是如果我的简洁,易于阅读的代码表现良好,那么我就把它留在那里。
类似的论点适用于对无点代码进行过分的推动。这是一种有用的技术,使用它会使许多功能变得更加简洁。但是它可以超出其用途。请注意,上述初始版本中的辅助函数addTo
并非没有意义。我们可以为其制作无积分版本。可能有更简单的方法,但是我想到的第一件事是pipe (lift (objOf) (prop ('prop'), prop ('value')), mergeAll)
。我们可以通过以下方式内联地编写此函数的完全无点版本:
const transform5 = pipe (
groupBy (prop ('name')),
map (pipe (
map (lift (objOf) (
prop ('prop'),
prop ('value')
)),
mergeAll
))
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}];
console .log (
transform5 (items)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, groupBy, prop, map, lift, objOf, mergeAll} = R </script>
这能给我们带来什么好处吗?我看不到。代码更复杂,表达能力也更差。这和for
循环变体一样难以理解。
因此,再次强调保持代码简单。这是我的建议,我坚持下去!
答案 1 :(得分:2)
我会改用reduceBy
:
const items = [
{name: 'n1', prop: 'p1', value: 90},
{name: 'b', prop: 'p2', value: 1},
{name: 'n1', prop: 'p3', value: 3}];
// {name: 'n1', prop: 'p1', value: 90} => {p1: 90}
const kv = obj => ({[obj.prop]: obj.value});
// {p1: 90}, {name: 'n1', prop: 'p3', value: 3} -> {p1: 90, p3: 3}
const reducer = (acc, obj) => mergeRight(acc, kv(obj));
console.log(
reduceBy(reducer, {}, prop('name'), items)
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {reduceBy, prop, mergeRight} = R;</script>
答案 2 :(得分:1)
一个命令性的for...of
循环,虽然有点冗长,但是性能却很不错。它具有可读性。
const fn = arr => {
const obj = {}
for(const { name, prop, value } of arr) {
if(!obj[name]) obj[name] = {} // initialize the group if it doesn't exist
obj[name][prop] = value // add the prop and it's value to the group
}
return obj
}
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]
const result = fn(items)
console.log(result)
使用Ramda的功能性解决方案速度较慢,但根据阵列中项目的数量,可以忽略不计。我通常从功能性解决方案开始,并且只有在遇到性能问题时,才进行配置,然后回退到性能更高的强制性选项。
使用Ramda的可读无点解决方案-R.groupBy和R.map将成为基础。在这种情况下,我将每个组项目映射到其道具,然后使用R.fromPairs生成对象。
const { pipe, groupBy, prop, map, props, fromPairs } = R
const fn = pipe(
groupBy(prop('name')),
map(pipe(
map(props(['prop', 'value'])),
fromPairs
))
)
const items = [{name: 'n1', prop: 'p1', value: 90}, {name: 'b', prop: 'p2', value: 1}, {name: 'n1', prop: 'p3', value: 3}]
const result = fn(items)
console.log(result)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>