简体   繁体   English

使用动态嵌套属性键对数组中的对象进行排序

[英]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 - 我们将用这些来回答这个问题 -

  1. Monads 单子
  2. Contravariant Functors 逆变函数

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 in Nullable types, thus has none of the problems associated with using null or undefined — like NullPointerException or TypeError . Maybe(a)显式地模拟Nullable类型中隐含的效果,因此没有与使用nullundefined相关的问题 - 如NullPointerExceptionTypeError

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? mapfilter函数如何生成数组? 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 . 当然ab有时是复杂的对象,所以我们不能直接调用ascdesc 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 - 如果您需要访问的嵌套属性不能保证是generalfileId ,我们可以创建一个接受属性列表的泛型函数,并且可以查找任何深度的嵌套属性 -

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 - 这个概念很简单,你可以在几十行中实现fromNullableJustNothing ,我们将在后面的答案中看到 -

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,或许还有一些意想不到的惊喜,请浏览以下相关答案 -

  1. multi-sort using contramap 使用contramap进行多重排序
  2. recursive search using contramap 使用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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM