[英]Sort objects in array with dynamic nested property keys
I'm trying to sort an array of nested objects. 我正在尝试对嵌套对象数组进行排序。 It's working with a static chosen key but I can't figure out how to get it dynamically. 它使用静态选择键,但我无法弄清楚如何动态获取它。
So far I've got this code 到目前为止,我已经有了这段代码
sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
At this point the keys are hardcoded ['general']['orderID']
but I want this part to be dynamic by adding a keys
param to the sortBy
function: 此时键是硬编码的['general']['orderID']
但我想通过向sortBy
函数添加keys
参数来使这部分变为动态:
sortBy = (keys, isReverse=false) => { ...
keys
is an array with the nested keys. keys
是一个带嵌套键的数组。 For the above example, it will be ['general', 'fileID']
. 对于上面的例子,它将是['general', 'fileID']
。
What are the steps that need to be taken to make this dynamic? 为实现这一目标需要采取哪些步骤?
Note: child objects can be undefined therefore I'm using a || {}
注意:子对象可以是未定义的,因此我使用的a || {}
a || {}
Note 2: I'm using es6. 注2:我正在使用es6。 No external packages. 没有外部包。
You can loop ovver the keys to get the values and then compare them like 您可以循环使用键来获取值,然后比较它们
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
The currently accepted answer apart from putting bugs in your code is not doing much to help you. 除了在您的代码中添加错误之外,当前接受的答案对您没有太大帮助。 Use of a simple function deepProp
would mitigate the painful repetition - 使用简单的函数deepProp
可以减轻痛苦的重复 -
const deepProp = (o = {}, props = []) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise - 现在没有那么多噪音 -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating the previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. 尽管如此,这在实际改进您的计划方面做的很少。 It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. 它充满了复杂性,更糟糕的是,这种复杂性将在任何需要类似功能的组件中重复出现。 React embraces functional style so this answer approaches the problem from a functional standpoint. React采用功能风格,因此这个答案从功能的角度来解决问题。 In this post, we'll write sortBy
as - 在这篇文章中,我们将sortBy
写为 -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; 你的问题让我们学习两个强大的功能概念; we'll use these to answer the question - 我们将用这些来回答这个问题 -
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. 让我们不要被术语所淹没,而是专注于获得对事物如何运作的直觉。 At first, it looks like we have a problem checking for nulls. 起初,看起来我们在检查空值时遇到了问题。 Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. 必须处理我们的一些输入可能没有嵌套属性的可能性使我们的函数混乱。 If we can generalize this concept of a possible value , we can clean things up a bit. 如果我们可以概括这个可能值的概念,我们可以稍微清理一下。
Your question specifically says you are not using an external package right now, but this is a good time to reach for one. 你的问题明确表示你现在没有使用外部包,但这是一个很好的时间。 Let's take a brief look at the data.maybe
package - 我们来看看data.maybe
包 -
A structure for values that may not be present, or computations that may fail. 可能不存在的值的结构,或可能失败的计算。
Maybe(a)
explicitly models the effects that implicit inNullable
types, thus has none of the problems associated with usingnull
orundefined
— likeNullPointerException
orTypeError
.Maybe(a)
显式地模拟Nullable
类型中隐含的效果,因此没有与使用null
或undefined
相关的问题 - 如NullPointerException
或TypeError
。
Sounds like a good fit. 听起来很合适。 We'll start by writing a function safeProp
that accepts an object and a property string as input. 我们首先编写一个函数safeProp
,它接受一个对象和一个属性字符串作为输入。 Intuitively, safeProp
safely returns the property p
of object o
- 直观地, safeProp
安全地返回对象o
的属性p
-
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p]
which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result - 而不是简单地返回可能是null或未定义值的o[p]
,我们将返回一个指导我们处理结果的Maybe -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in - 现在我们有一个函数可以处理不同复杂程度的对象并保证我们感兴趣的结果 -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. 这就是那里的一半战斗。 We can now go from our complex object to the precise string value we want to use for comparison purposes. 我们现在可以从复杂对象转到我们想要用于比较目的的精确字符串值。
I'm intentionally avoiding showing you an implementation of Maybe
here because this in itself is a valuable lesson. 我故意避免在这里向你展示Maybe
的实现,因为这本身就是一个宝贵的教训。 When a module promises capability X , we assume we have capability X , and ignore what happens in the black box of the module. 当模块承诺能力X时 ,我们假设我们有能力X ,并忽略模块黑盒中发生的事情。 The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level. 数据抽象的关键在于隐藏关注点,以便程序员可以在更高层次上思考问题。
It might help to ask how does Array work? 可能有助于询问Array如何工作? How does it calculate or adjust the length
property when an element is added or removed from the array? 当从数组中添加或删除元素时,它如何计算或调整length
属性? How does the map
or filter
function produce a new array? map
或filter
函数如何生成新数组? If you never wondered these things before, that's okay! 如果你以前从未想过这些东西,那没关系! Array is a convenient module because it removes these concerns from the programmer's mind; Array是一个方便的模块,因为它从程序员的脑海中消除了这些问题; it just works as advertised . 它就像宣传的那样工作 。
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. 无论模块是由JavaScript提供,还是由第三方提供,例如来自npm,或者您自己编写模块,这都适用。 If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. 如果Array不存在,我们可以将它实现为我们自己的数据结构,具有相同的便利性。 Users of our module gain useful functionalities without introducing additional complexity. 我们模块的用户获得了有用的功能, 而没有引入额外的复 The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. 当你意识到程序员是他/她自己的用户时,a-ha时刻到来了:当你遇到一个棘手的问题时,写一个模块让你摆脱复杂的束缚。 Invent your own convenience! 发明自己的便利!
We'll show a basic implementation of Maybe later in the answer, but for now, we just have to finish the sort ... 我们将在后面的答案中展示Maybe的基本实现,但是现在,我们只需完成排序......
We start with two basic comparators, asc
for ascending sort, and desc
for descending sort - 我们从两个基本的比较器开始, asc
用于升序排序, desc
用于降序排序 -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate the previous state, instead, we must create new state. 在React中,我们不能改变先前的状态,相反,我们必须创建新的状态。 So to sort immutably , we must implement isort
which will not mutate the input object - 因此,要进行不可分类的排序,我们必须实现不会改变输入对象的isort
-
const isort = (compare = asc, xs = []) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a
and b
are sometimes complex objects, so case we can't directly call asc
or desc
. 当然a
和b
有时是复杂的对象,所以我们不能直接调用asc
或desc
。 Below, contramap
will transform our data using one function g
, before passing the data to the other function, f
- 下面, contramap
将使用一个函数g
转换我们的数据, 然后将数据传递给另一个函数f
-
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc
, we can see sorting work in the other direction - 使用其他比较器desc
,我们可以看到排序工作在另一个方向 -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy
. 现在编写React组件的方法sortBy
。 The method is essentially reduced to this.setState({ files: t (this.state.files) })
where t
is an immutable transformation of your program's state. 该方法基本上简化为this.setState({ files: t (this.state.files) })
其中t
是程序状态的不可变转换。 This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test - 这很好,因为在测试困难的情况下,复杂性会被排除在组件之外,而是存在于易于测试的通用模块中 -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function - 这使用了原始问题中的布尔开关,但由于React包含功能模式,我认为它作为高阶函数会更好 -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general
and fileId
, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth - 如果您需要访问的嵌套属性不能保证是general
和fileId
,我们可以创建一个接受属性列表的泛型函数,并且可以查找任何深度的嵌套属性 -
const deepProp = (o = {}, props = []) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe
package which provides us with the capability to work with potential values . 上面,我们使用data.maybe
包,它为我们提供了处理潜在价值的能力。 The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. 模块导出函数以将普通值转换为Maybe,反之亦然,以及许多适用于潜在值的有用操作。 There's nothing forcing you to use this particular implementation, however. 但是,没有什么可以迫使您使用这个特定的实现。 The concept is simple enough that you could implement fromNullable
, Just
and Nothing
in a couple dozen lines, which we'll see later in this answer - 这个概念很简单,你可以在几十行中实现fromNullable
, Just
和Nothing
,我们将在后面的答案中看到 -
Run the complete demo below on repl.it 在repl.it上运行下面的完整演示
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = []) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages of this approach should be evident. 这种方法的优点应该是显而易见的。 Instead of one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. 我们结合了几个更易于编写,读取和测试的小函数,而不是一个难以编写,读取和测试的大型复杂函数。 The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part. 较小的函数具有在程序的其他部分中使用的附加优点,而大的复杂函数可能仅在一个部分中可用。
Lastly, sortBy
is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse
boolean; 最后, sortBy
实现为高阶函数,这意味着我们不仅限于通过reverse
布尔切换的升序和降序排序; any valid comparator can be used. 可以使用任何有效的比较器。 This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year
first, then month
, then day
, etc; 这意味着我们甚至可以编写使用自定义逻辑来处理领带减免或比较专门的比较year
第一,然后month
,然后day
,等等; higher-order functions expand your possibilities tremendously. 高阶函数极大地扩展了您的可能性。
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe
. 我不喜欢做空的承诺所以我想告诉你,设置像Maybe
这样的机制并不困难。 This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. 这也是数据抽象的一个很好的教训,因为它向我们展示了一个模块如何有自己的关注点。 The module's exported values are the only way to access the module's functionalities; 模块的导出值是访问模块功能的唯一方法; all other components of the module are private and are free to change or refactor as other requirements dictate - 该模块的所有其他组件都是私有的,可以根据其他要求自由更改或重构 -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. 然后我们将在我们的模块中使用它。 We only have to change the import ( require
) but everything else just works as-is because of the public API of our module matches - 我们只需更改导入( require
),但其他所有内容都按原样工作,因为我们模块的公共API匹配 -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers - 如需更直观地了解如何使用Contramap,或许还有一些意想不到的惊喜,请浏览以下相关答案 -
You can use a loop to extract a nested property path from an object: 您可以使用循环从对象中提取嵌套属性路径:
const obj = { a: { b: { c: 3 } } } const keys = ['a', 'b', 'c'] let value = obj; for (const key of keys) { if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only value = value[key]; } console.log(`c=${value}`);
Then you can wrap the function above into a helper: 然后你可以将上面的函数包装成一个帮助器:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values: 并在获取您的价值时使用它:
sortBy = (isReverse = false, keys = []) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce()
to traverse deeply into nested objects. 要使用任意数量的键,您可以创建一个可以与.reduce()
重用的函数,以深入遍历嵌套对象。 I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax. 我还将键作为最后一个参数,以便您可以使用“rest”和“spread”语法。
const getKey = (o, k) => (o || {})[k]; const sorter = (isReverse, ...keys) => (a, b) => { const valueA = keys.reduce(getKey, a) || ''; const valueB = keys.reduce(getKey, b) || ''; if (isReverse) return valueB.localeCompare(valueA); return valueA.localeCompare(valueB); }; const sortBy = (isReverse = false, ...keys) => { this.setState(prevState => ({ files: prevState.files.sort(sorter(isReverse, ...keys)) })); }
I also moved the sort function out to its own const
variable, and made it return a new function that uses the isReverse
value. 我还将sort函数移出到它自己的const
变量,并使它返回一个使用isReverse
值的新函数。
One way could be using reduce() over the new keys
argument, something like this: 一种方法是使用reduce()而不是新的keys
参数,如下所示:
sortBy = (keys, isReverse=false) =>
{
this.setState(prevState =>
({
files: prevState.files.slice().sort((a, b) =>
{
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueB = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
return (isReverse ? valueB.localeCompare(valueA) : valueA.localeCompare(valueB));
})
}));
}
Compare elements in sort function in following way: 以下列方式比较sort函数中的元素:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this: 喜欢这个:
sortBy = (keys, isReverse=false) => { this.setState(prevState => ({ files: prevState.files.sort((a, b) => { let v=c=>keys.reduce((o,k) => o[k]||'',c) return (isReverse ? -1 : 1)*v(a).localeCompare(v(b)); }) })); }
Here is example how this idea works: 以下是这个想法的工作原理:
let files = [ { general: { fileID: "3"}}, { general: { fileID: "1"}}, { general: { fileID: "2"}}, { general: { }} ]; function sortBy(keys, arr, isReverse=false) { arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) => (isReverse ? -1 : 1)*v(a).localeCompare(v(b)) ) } sortBy(['general', 'fileID'],files,true); console.log(files);
This also handles the case when the path resolves to a non-string value by converting it to string. 这也处理了路径通过将其转换为字符串而解析为非字符串值的情况。 Otherwise .localeCompare
might fail. 否则.localeCompare
可能会失败。
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.