介绍
这是一篇短文,旨在展示多种在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
提供了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
函数(同样传入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地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
最后,我们还可以使用Lenses
。Ramda
就带有lensProp
和lensPath
// lensesconst 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]