简介
这是一篇简短的文章,旨在展示在JavaScript中安全地访问深度嵌套值的许多不同方法。以下示例都做同样的事情,虽然它们的方法可能不同,但它们都解决了同样的问题。
在开始之前,让我们更详细地了解一下我们实际上要解决的问题。
const props = {
user: {
posts: [
{ title: 'Foo', comments: [ 'Good one!', 'Interesting...' ] },
{ title: 'Bar', comments: [ 'Ok' ] },
{ title: 'Baz', comments: [] },
]
}
}
想象一下我们有一个名为_props_的对象,它可能看起来像上面的例子。现在假设我们想获得用户第一篇帖子的评论。我们如何使用常规的JavaScript来实现这一点?
// access deeply nested values...
props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments
这可能看起来像我们可能会遇到的问题,并且有道理,我们要确保在尝试访问它之前存在键或索引。进一步思考,假设我们还想仅读取第一条评论。为了解决这个问题,我们可能会更新我们之前的示例,以在访问第一个项目之前也检查评论是否实际存在。
// updating the previous example...
props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments &&
props.user.posts[0].comments[0]
因此,每当我们想访问任何深度嵌套的数据时,我们都必须明确进行手动检查。 为了说明为什么这很麻烦,想象一下我们不想检查用户的帖子,而是想知道用户最后一条评论的博客标题。我们将无法建立在之前的示例之上。
// accessing user's comments
props.user &&
props.user.comments &&
props.user.comments[0] &&
props.user.comments[0].blog.title
示例可能有些夸张,但你懂的。我们需要逐级检查整个结构,直到达到我们正在搜索的值。
好的,既然我们更好地了解了我们实际上要解决的问题,让我们看看我们可以采用的不同方法。示例从仅使用JavaScript开始,然后是Ramda,最后是Ramda与Folktale。虽然你可能不需要更高级的示例,但这应该是有趣的。特别是考虑到在访问深度嵌套的值时采用安全方法所获得的收益。
简化的JavaScript
为了开始,我们不想手动检查可空或未定义值,而是可以部署自己的小型但紧凑的函数,并且可以灵活处理任何提供的输入数据。
const get = (p, o) =>
p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o)// let's pass in our props object...console.log(get(['user', 'posts', 0, 'comments'], props))
// [ 'Good one!', 'Interesting...' ]console.log(get(['user', 'post', 0, 'comments'], props))
// null
让我们分解一下我们的get函数。
const get = (p, o) =>
p.reduce((xs, x) =>
(xs && xs[x]) ? xs[x] : null, o)
我们将一个path定义作为第一个参数传递,将要从中检索值的对象作为第二个参数传递。
关于第二个参数是对象的事实,你可能会问自己:我们从中获得了什么? 定义一个通用函数,该函数知道特定路径并期望任何可能或可能没有给定路径的对象。
通过选择这种方法,我们可以使用我们以前的_props_对象或任何其他对象调用_getUserComments_。这还意味着我们必须像这样对我们的get函数进行编译。
const get = p => o =>
p.reduce((xs, x) =>
(xs && xs[x]) ? xs[x] : null, o)
最后,我们可以记录结果并验证是否按预期工作。
console.log(getUserComments(props))
// [ 'Good one!', 'Interesting...' ]console.log(getUserComments({user:{posts: []}}))
// null
我们的_get_函数本质上是提供的路径上的减少。
p.reduce((xs, x) =>
(xs && xs[x]) ? xs[x] : null, o)
让我们采用简化的路径,其中我们只想访问_id_。
['id'].reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, {id: 10})
我们使用提供的对象初始化_reduce_函数,然后检查对象是否已定义,如果是,则验证键是否存在。根据(xs && xs[x])的结果,我们返回值或null等等。
如所示,我们可以轻松地缓解必须隐式检查任何可空或未定义值的问题。如果你宁愿传递字符串路径而不是数组,则_get_函数只需要进行一些较小的调整,我将留给感兴趣的读者来实现。
Ramda
我们可以使用Ramda函数来实现相同的功能,而不是编写自己的函数。
Ramda自带的一个函数是path。 _path_需要两个参数,路径和对象。让我们在Ramda中重新编写示例。
const getUserComments = R.path(['user', 'posts', 0, 'comments'])
现在,我们可以使用_props_调用_getUserComments_,无论是否找到所需的值,都会返回null。
getUserComments(props) // [ 'Good one!', 'Interesting...' ]getUserComments({}) // null
但是,如果我们想在找不到指定路径时返回与null不同的内容,Ramda还提供了pathOr。 _pathOr_需要默认值作为初始参数。
const getUserComments = R.pathOr([], ['user', 'posts', 0, 'comments'])getUserComments(props) // [ 'Good one!', 'Interesting...' ]getUserComments({}) // []
感谢Gleb Bahmutov提供有关path和pathOr的见解。
Ramda + Folktale
让我们还将Folktale的Maybe加入到混合中。例如,我们可以构建一个通用的getPath函数,该函数期望路径以及我们要从中检索值的对象。
const getPath = R.compose(Maybe.fromNullable, R.path)const userComments =
getPath(['user', 'posts', 0, 'comments'], props)
调用_getPath_将返回Maybe.Just或Maybe.Nothing。
console.log(userComments) // Just([ 'Good one!', 'Interesting...' ])
**通过将结果包装在Maybe中,我们获得了什么?**采用这种方法,我们现在可以安全地继续使用_userComments_,而无需手动检查userComments是否返回null或所需值。
console.log(userComments.map(x => x.join(',')))
// Just('Good one!,Interesting...')
当找不到值时,同样的方法也适用。
const userComments =
getPath(['user', 'posts', 8, 'title'], props)
console.log(userComments.map(x => x.join(',')).toString())
// Nothing
查看示例。我们还可以将所有的props包装在Maybe中。这使我们能够使用composeK,它知道如何将props链接在一起。
// example using composeK to access a deeply nested value.const getProp = R.curry((name, obj) =>
Maybe.fromNullable(R.prop(name, obj)))const findUserComments = R.composeK(
getProp('comments'),
getProp(0),
getProp('posts'),
getProp('user')
)console.log(findUserComments(props).toString())
// Just([ 'Good one!', 'Interesting...' ])console.log(findUserComments({}).toString())
// Nothing
这些都非常高级,使用Ramda的path应该足够了。但是让我们快速看一下其他示例。例如,我们也可以使用Ramda的compose和chain来实现与上面示例相同的效果。
// using compose and chainconst getProp = R.curry((name, obj) =>
Maybe.fromNullable(R.prop(name, obj)))const findUserComments =
R.compose(
R.chain(getProp('comments')),
R.chain(getProp(0)),
R.chain(getProp('posts')),
getProp('user')
)console.log(findUserComments(props).toString())
// Just([ 'Good one!', 'Interesting...' ])console.log(findUserComments({}).toString())
// Nothing
与以前的示例相同,但这次使用pipeK。
// example using pipeK to access a deeply nested value.const getProp = R.curry((name, obj) =>
Maybe.fromNullable(R.prop(name, obj)))const findUserComments = R.pipeK(
getProp('user'),
getProp('posts'),
getProp(0),
getProp('comments')
)console.log(findUserComments(props).toString())
// Just([ 'Good one!', 'Interesting...' ])console.log(findUserComments({}).toString())
// Nothing
还可以通过使用map的pipeK示例进行检查。感谢Tom Harding提供的pipeK示例。
镜头
最后,我们还可以使用lenses。Ramda带有lensProp和lensPath。
// lenses
const findUserComments =
R.lensPath(['user', 'posts', 0, 'comments'])console.log(R.view(findUserComments, props))
// [ 'Good one!', 'Interesting...' ]
同样,我们可以像我们的path示例一样将结果包装在Maybe中。当需要更新任何嵌套值时,lenses非常有用。有关lenses的更多信息,请查看我的JavaScript中的Lenses简介文章。
总结
处理嵌套数据时,我们应该对许多检索值的方式有一个良好的概述。除了知道如何推出我们自己的实现之外,我们还应该基本了解Ramda提供的有关此问题的函数,并且甚至可能更好地了解为什么将结果包装在Either或Maybe中是有意义的。此外,我们还触及了_lenses_主题,除了使我们能够检索任何值之外,还使我们能够更新深度嵌套的数据而不会使我们的对象发生变异。
最后,你不应该再编写以下代码了。
// updating the previous example...props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments &&
props.user.posts[0].comments[0]
特别感谢Gleb Bahmutov、Tom Harding和Toastal在Twitter上提供的示例和见解。
更新:2017年3月24日
Gleb Bahmutov发表了Call me Maybe,将这里讨论的概念推进了一步。强烈推荐阅读。



评论(0)