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

时间:2019-02-17 17:53:35

标签: javascript arrays sorting object ecmascript-6

我正在尝试对嵌套对象的数组进行排序。它正在使用静态选择的键,但我不知道如何动态获取它。

到目前为止,我已经有了这段代码

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);
        })
    }));
}

这时,键是硬编码的['general']['orderID'],但我希望通过向keys函数中添加sortBy参数来使该部分具有动态性:

sortBy = (keys, isReverse=false) => { ...

keys是带有嵌套键的数组。对于上面的示例,它将为['general', 'fileID']

要使其动态化,需要采取哪些步骤?

注意:子对象可以是未定义的,因此我正在使用a || {}

注2:我正在使用es6。没有外部软件包。

7 个答案:

答案 0 :(得分:4)

您可以循环遍历所有键以获取值,然后像进行比较

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);
        })
    }));
}

答案 1 :(得分:3)

putting bugs in your code外,当前接受的答案对您没有多大帮助。使用简单的功能deepProp将减轻痛苦的重复-

const deepProp = (o = {}, props = []) =>
  props.reduce((acc = {}, p) => acc[p], o)

现在没有太多噪音-

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)
      })
  })

尽管如此,这实际上并没有改善程序。它充满了复杂性,更糟的是,这种复杂性将在需要类似功能的任何组件中重复出现。 React包含功能样式,因此此答案从功能角度解决了问题。在这篇文章中,我们将sortBy写为-

sortBy = (comparator = asc) =>
  this.setState
    ( { files:
          isort
            ( contramap
                ( comparator
                , generalFileId
                )
            , this.state.files
            )
      }
    )

您的问题使我们学习了两个强大的功能概念;我们将用它们来回答问题-

  1. Monads
  2. 逆变函子

不过,我们不要被术语淹没,而应该专注于获得事物运作方式的直觉。起初,看起来我们在检查null时遇到问题。必须处理某些输入可能没有嵌套属性的可能性,使我们的函数混乱。如果我们可以概括这个可能值的概念,那么我们可以进行一些整理。

您的问题专门说您现在不使用外部程序包,但这是一个很好的机会。让我们简要介绍一下data.maybe软件包-

  

用于可能不存在的值或可能失败的计算的结构。 Maybe(a)显式建模Nullable类型中隐含的效果,因此不存在与使用nullundefined相关的问题-像NullPointerException或{{1 }}。

听起来很合适。我们将首先编写一个函数TypeError,该函数接受一个对象和一个属性字符串作为输入。直观地讲,safeProp 安全返回对象safeProp-

的属性p
o

我们将返回一个也许,它指导我们处理结果,而不是简单地返回{em>可能为为空或未定义的值的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 () -

o[p]

现在我们有了一个函数,可以获取复杂程度各异的对象,并保证我们感兴趣的结果-

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 ('') 

那是战斗的一半。现在,我们可以从复杂的对象转到要用于比较目的的精确字符串值。

在这里我特意避免向您展示console .log ( generalFileId ({ general: { fileId: 'a' } }) // 'a' , generalFileId ({ general: { fileId: 'b' } }) // 'b' , generalFileId ({ general: 'x' }) // '' , generalFileId ({ a: 'x '}) // '' , generalFileId ({ general: { err: 'x' } }) // '' , generalFileId ({}) // '' ) 的实现,因为这本身就是很有价值的一课。当模块承诺功能 X 时,我们假定我们具有功能 X ,而忽略模块黑盒中发生的情况。数据抽象的重点是将隐患隐藏起来,以便程序员可以在更高层次上思考问题。

询问数组如何工作可能会有所帮助?从数组中添加或删除元素时,它如何计算或调整Maybe属性? lengthmap函数如何产生一个 new 数组?如果您以前从未想过这些事情,那没关系!数组是一个方便的模块,因为它使程序员无需担心这些问题。它只是按广告起作用

无论模块是由JavaScript提供还是由第三方(例如npm)提供,还是您自己编写模块,都适用。如果Array不存在,则可以使用等效的便利将其实现为我们自己的数据结构。我们模块的用户可以 获得有用的功能,而不会增加额外的复杂性。当意识到程序员是他/她自己的用户时,就发生了错觉:当您遇到棘手的问题时,编写一个模块以使自己摆脱复杂的束缚。 Invent your own convenience!

我们稍后会在答案中展示Maybe的基本实现,但现在,我们只需要完成排序即可。


我们从两个基本比较器开始,filter用于升序排序,asc用于降序排序-

desc

在React中,我们不能改变先前的状态,相反,我们必须创建 new 状态。因此,要对不可变进行排序,我们必须实现const asc = (a, b) => a .localeCompare (b) const desc = (a, b) => asc (a, b) * -1 ,它不会使输入对象发生突变-

isort

当然,const isort = (compare = asc, xs = []) => xs .slice (0) // clone .sort (compare) // then sort a有时是复杂的对象,因此,不能直接调用basc。下面,desc将使用一个函数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' } } // ] ,我们可以看到另一个方向的排序工作-

desc

现在为您的React组件isort ( contramap (desc, generalFileId) // descending comparator , files ) // [ { general: { fileId: 'e' } } // , { general: { fileId: 'd' } } // , { general: { fileId: 'c' } } // , { general: { fileId: 'b' } } // , { general: { fileId: 'a' } } // ] 编写方法。该方法实质上简化为sortBy,其中this.setState({ files: t (this.state.files) })是程序状态的不变转换。这样做很好,因为复杂度可以在难以测试的组件中保持在 之外,而可以驻留在易于测试的通用模块中-

t

这使用了像您原始问题中那样的布尔开关,但是由于React包含功能模式,所以我认为作为高阶函数会更好-

sortBy = (reverse = true) =>
  this.setState
    ( { files:
          isort
            ( contramap
                ( reverse ? desc : asc
                , generalFileId
                )
            , this.state.files
            )
      }
    )

如果不能保证您需要访问的嵌套属性为sortBy = (comparator = asc) => this.setState ( { files: isort ( contramap ( comparator , generalFileId ) , this.state.files ) } ) general,我们可以制作一个泛型函数来接受属性的 list 并可以查找任何深度的嵌套属性-

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 ) 软件包,该软件包使我们能够使用潜在值。该模块导出函数以将普通值转换为Maybe,反之亦然,以及导出许多适用于潜在值的有用操作。但是,没有什么强迫您使用此特定实现。这个概念很简单,您可以在几十行中实现data.maybefromNullableJust,我们将在此答案的后面部分进行介绍-

repl.it上运行下面的完整演示

Nothing

这种方法的优点应该显而易见。代替了一个很难编写,阅读和测试的大型复杂函数,我们结合了几个更易于编写,阅读和测试的较小函数。较小的函数具有在程序的其他部分中使用的附加优点,而较大的复杂函数可能仅在一部分中可用。

最后,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' } } // ] 被实现为高阶函数,这意味着我们不仅限于由sortBy布尔值切换的升序和降序排序;可以使用任何有效的比较器。这意味着我们甚至可以编写一个专门的比较器,使用自定义逻辑处理平局决胜局,或者先比较reverse,然后比较year,然后比较month,等等;高阶函数极大地扩展了您的可能性。


我不喜欢虚空承诺,因此我想告诉你,设计自己的机制day并不困难。这也是数据抽象的不错的一课,因为它向我们展示了模块如何具有自己的关注点。模块的导出值是访问模块功能的唯一方法。该模块的所有其他组件都是私有的,并且可以根据其他要求自由更改或重构-

Maybe

然后我们将在模块中使用它。我们只需要更改导入(// 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 ),但其他一切都照常进行,因为我们的模块的公共API与-

require

有关如何使用对比度图的更多直觉,也许还有一些意想不到的惊喜,请浏览以下相关答案-

  1. multi-sort using contramap
  2. recursive search using contramap

答案 2 :(得分:2)

您可以使用循环从对象中提取嵌套的属性路径:

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}`);

然后,您可以将上面的函数包装到一个帮助器中:

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;
}

并在获取值时使用它:

sortBy = (isReverse = false, keys = []) => {
  this.setState(prevState => ({
    files: prevState.files.sort((a, b) => {
      const valueA = getPath(a, keys) || '';
      const valueB = getPath(b, keys) || '';

      // ...
    })
  }));
}

答案 3 :(得分:2)

一种方法是在新的keys参数上使用reduce(),如下所示:

sortBy = (keys, isReverse=false) => {
    this.setState(prevState => ({
        files: prevState.files.sort((a, b) => {
            const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
            const valueA = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();

            if (isReverse) return valueB.localeCompare(valueA);

            return valueA.localeCompare(valueB);
        })
    }));
}

答案 4 :(得分:1)

要使用任意数量的键,您可以创建一个可以与.reduce()重用的函数,以深入遍历嵌套对象。我还将密钥作为最后一个参数,以便可以使用“ 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))
  }));
}

我还将sort函数移到了自己的const变量中,并使其返回了一个使用isReverse值的新函数。

答案 5 :(得分:0)

按以下方式比较排序函数中的元素:

let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));

赞一下:

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));
        })
    }));
}

以下是此想法如何工作的示例:

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);

答案 6 :(得分:0)

这也处理路径通过将其转换为字符串解析为非字符串值的情况。否则.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();
}