除了在你的代码中放置错误,目前被接受的答案并没有太大帮助。使用一个简单的函数deepProp
可以减轻繁琐的重复 -
const deepProp = (o = {}, props = []) =>
props.reduce((acc = {}, p) => acc[p], o)
现在没有那么多噪音了 -
sortBy = (keys, isReverse = false) =>
this.setState ({
files:
[...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
)
}
)
您的问题让我们学习了两个强大的函数概念; 我们将使用它们来回答问题 -
- 单子(Monads)
- 逆变函子(Contravariant Functors)
让我们不要被术语所压倒,而是专注于获得对事物如何工作的直觉。起初,看起来我们有一个检查空值的问题。必须处理一些输入可能没有嵌套属性的可能性使我们的函数变得混乱。如果我们可以概括这个“可能值”的概念,我们就可以稍微整理一下。
您的问题特别指出您现在没有使用外部包,但这是使用外部包的好时机。让我们简要地看一下
data.maybe
包-
引用:
“用于可能不存在的值或可能失败的计算的结构。`Maybe(a)`明确地模拟了隐含在`Nullable`类型中的效果,因此没有使用`null`或`undefined`带来的问题-例如`NullPointerException`或`TypeError`。”
听起来很合适。我们将从编写一个函数safeProp
开始,该函数接受一个对象和一个属性字符串作为输入。直观地说,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 ()
我们不仅会返回可能是null或undefined值的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 ('')
现在我们有一个函数,它可以接受不同复杂度的对象,并且保证我们感兴趣的结果。
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
这已经解决了一半的问题。现在我们可以从复杂的对象中获取精确的字符串值,以供比较使用。
我有意避免在此展示Maybe
的实现,因为这本身就是一个有价值的教训。当一个模块承诺能力X时,我们假设我们拥有能力X,并忽略模块内部黑盒子中发生的事情。数据抽象的关键之处在于隐藏问题,以便程序员可以以更高的层次思考问题。
也许问一下Array是如何工作的会有所帮助?当向数组添加或删除元素时,它如何计算或调整length
属性?map
或filter
函数如何产生一个新的数组?如果你以前从未想过这些问题,那没关系!数组是一个方便的模块,因为它从程序员的思维中移除了这些问题;它只是像广告中说的那样工作。
无论 JavaScript 提供模块,第三方库(例如 npm)提供模块,还是你自己编写模块,这都适用。如果没有数组存在,我们可以实现自己的数据结构,并具备等效的便利性。我们模块的用户获得了有用的功能,
而不会引入额外的复杂性。当你意识到程序员是自己的用户时,就会有顿悟时刻:当你遇到棘手的问题时,编写一个模块,使自己摆脱复杂性的枷锁。
发明自己的便利!
我们将在答案后面展示 Maybe 的基本实现,但现在,我们只需完成排序即可...
我们从两个基本比较器开始,
asc
表示升序排序,
desc
表示降序排序。
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
在React中,我们不能改变之前的状态,而是必须创建新的状态。因此,为了进行不可变的排序,我们必须实现
isort
,它不会改变输入对象。
const isort = (compare = asc, xs = []) =>
xs
.slice (0) // clone
.sort (compare) // then sort
当然,
a
和
b
有时是复杂对象,因此我们无法直接调用
asc
或
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)
, files
)
使用另一个比较器
desc
,我们可以看到排序按相反方向工作 -
isort
( contramap (desc, generalFileId)
, files
)
现在为您的React组件编写方法
sortBy
。该方法基本上被简化为
this.setState({files:t(this.state.files)})
,其中
t
是程序状态的不可变转换。这很好,因为复杂性被保持在难以测试的组件之外,并且它驻留在通用模块中,这些模块易于测试 -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
这个示例使用了类似于您原始问题中的布尔开关,但是由于React采用函数式编程模式,我认为将其作为高阶函数会更好 -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
如果您需要访问的嵌套属性不能保证是
general
和
fileId
,我们可以编写一个通用函数,它接受一个属性
列表并且可以查找任意深度的嵌套属性 -
const deepProp = (o = {}, props = []) =>
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ])
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ])
.getOrElse (0)
console.log
( generalFileId ({ general: { fileId: 'a' } } )
, generalFileId ({})
, fooBarQux ({ foo: { bar: { qux: 1 } } } )
, fooBarQux ({ foo: { bar: 2 } })
, fooBarQux ({})
)
上面,我们使用了
data.maybe
包,它为我们提供了处理
可能值 的能力。该模块导出了将普通值转换为 Maybe,反之亦然的函数,以及许多适用于可能值的有用操作。然而,并没有强制要求您使用这个特定的实现。这个概念非常简单,您可以用几十行代码实现
fromNullable
、
Just
和
Nothing
,我们稍后在本答案中会看到 -
在 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
)
这种方法的优点是显而易见的。我们将一个难以编写、阅读和测试的大型复杂函数转化为多个较小的函数,更容易编写、阅读和测试。较小的函数还有另一个优点,就是可以在程序的其他部分中使用,而大型复杂函数只能在一个部分中使用。
最后,sortBy作为高阶函数实现,这意味着我们不仅限于升序和降序排序,还可以使用任何有效的比较器。这意味着我们甚至可以编写一个专门处理自定义逻辑或先比较年份,然后比较月份,再比较日期等的特殊比较器;高阶函数极大地扩展了您的可能性。
我不喜欢空口承诺,所以我想向您展示像
Maybe
这样设计自己的机制并不困难。这也是数据抽象的一个很好的教训,因为它向我们展示了一个模块具有自己的一套关注点。模块的导出值是访问模块功能的唯一途径;模块的所有其他组件都是私有的,并且可以根据其他需求自由更改或重构。
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 }
然后我们将在模块中使用它。我们只需要更改导入 (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)
)
// ...
如果想更好地理解如何使用contramap,或者发现一些意外的惊喜,请查看以下相关答案:
- 使用contramap进行多重排序
- 使用contramap进行递归搜索