在javascript中安全地访问深层嵌套的值

介绍

这是一篇短文,旨在展示多种在javascript中安全地访问深层嵌套值的方式。
下面的例子通过不同的方式来解决这一问题。

开始之前,让我们看下实际遇到这种状况时..

假设有一个props对象(如下),当我们需要获取user对象的posts的第一条的comments对象,通常会如何操作?

const props = {
  user: {
    posts: [
      { title: 'Foo', comments: [ 'Good one!', 'Interesting...' ] },
      { title: 'Bar', comments: [ 'Ok' ] },
      { title: 'Baz', comments: [] },
    ]
  }
}
// access deeply nested values...
props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments

最直白的方式是确保每一个key或index的存在再去访问它的下一级。考虑的多点,当需求变化需要去请求第一条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]

所以每次我们想要访问深度嵌套的数据时,都必须明确地进行手动检查。或许很难说清这一点,试想一下当我们不希望检验users对象下的posts,只是希望获取到users下的最后一条comment,和前面的解决思路是相违背的。

这个例子可能有些夸张,但你懂我的意思,为了得到深层嵌套的值,我们需要检验整个结构的每一级(所有父级)。

所以,现在我们已经更好地理解了实际想要解决的问题,让我们来看看不同的解决方案。前面一些是通过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)作为第一个参数,需要获取的对象(object)作为第二个参数。
思考一下这第二个参数o(object),你可能会问自己:我们期望这个方法有什么功能?应该是一个输入特定路径并且针对任何对象都能返回是否存在预期对象的方法。

const get = p => o =>
  p.reduce((xs, x) =>
    (xs && xs[x]) ? xs[x] : null, o)

const getUserComments = get(['user', 'posts', 0, 'comments'])

通过这种方式,我们可以调用getUserComments和之前的props对象或是任何其他对象。这也暗示我们必须得像这样不停琢磨这个get函数,

最终我们能打印出结果,验证下是否如预期得结果。

console.log(getUserComments(props))
// [ 'Good one!', 'Interesting...' ]
console.log(getUserComments({user:{posts: []}}))
// null

get函数实质上就是在减少先前的路径。

让我们来简化一下,现在我们只想访问这个id。

['id'].reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, {id: 10})

我们用提供的对象初始化reduce函数,每一层通过(xs && xs[x]) 检验对象是否被定义且有效, 然后依次递归或是返回null退出。
就像上面的例子一样,我们可以轻巧地解决这一问题。当然如果你偏向习惯用字符串路径而不是数组来表达路径,还需要对get函数做一些小改动,我将留给感兴趣的读者来实现。

Ramda

我们也可以利用Ramda函数库来实现相同的功能,而不是编写自己的函数。
Ramda提供了一个path方法,两个参数输入, path以及object。让我们用Ramda重写这个例子。

const getUserComments = R.path(['user', 'posts', 0, 'comments'])

现在通过getUserComments传入数据源就能得到我们希望的值,如果没有找到就会得到null

getUserComments(props) // [ 'Good one!', 'Interesting...' ]
getUserComments({}) // null

但是如果我们想要返回的无效值不是null呢?Ramda提供了pathOrpathOr需要传入默认值作为参数。

const getUserComments = R.pathOr([], ['user', 'posts', 0, 'comments'])
getUserComments(props) // [ 'Good one!', 'Interesting...' ]
getUserComments({}) // []

感谢Gleb Bahmutov提供对于path和pathOr的见解。

Ramda + Folktale

让我们再加入FolktaleMaybe。例如我们可以构建一个更通用的getPath函数(同样传入path和object)。

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是否返回nul。

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

我们可以把所有属性包裹在Maybe内。这使我们能够使用composeK来实现链式调用。

// 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地composechain实现同样的效果)

// using compose and chain
const 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

最后,我们还可以使用LensesRamda就带有lensProplensPath

// lenses
const findUserComments =
  R.lensPath(['user', 'posts', 0, 'comments'])
console.log(R.view(findUserComments, props))
// [ 'Good one!', 'Interesting...' ]

总结

我们应该对如何检索嵌套数据的多种方法有了清楚的理解。除了知道如何自己实现外,还应该对Ramda提供的关于这个问题的功能有一个基本的了解。甚至可以更好地i理解为什么将结果包含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]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值