无报错链式取值的几种方法
先看下面这段代码:
const MyComponent = props => (
<div>{props.user.info.addresss[0].city || '暂无城市信息'}</div>
)
这段代码平平无奇,但是同样的,下面这段配套报错也十分常见:
Uncaught TypeError: Cannot read property '0' of undefined
会是什么原因造成的报错不需要赘述,直观的解决方法有二:通过人治手段约束数据所有层不得为空,或者像如下多加一些判断:
const MyComponent = props => (
<div>
{(props.user.info.addresss && props.user.info.addresss[0].city) || '暂无城市信息'}
</div>
)
不得不说,这代码写着很累,看着很丑,更别论这段代码里有好几层取值,每一层都可能报错,都判断的话代码就成一坨****。
这种问题也谈不上多大,但是对于追求优雅的高级程序员来说一定希望有能work而且不ugly的办法。方法很多,我比较喜欢的有两种,然后自己又用Proxy实现了另一种。
方法一:Optional Chaining
这是一个当前处于stage 2的ecma新语法,babel官方已经实现了语法插件,不过目前还没放到自己的哪个presets里,可以直接通过plugins使用:babel-plugin-transform-optional-chaining
按照这种语法代码就可以这么写:
const MyComponent = props => (
<div>
{props?.user?.info?.addresss?.[0]?.city || '暂无城市信息'}
</div>
)
只要能习惯“?”的存在,没什么问题。
【此处有过更正】经过提醒,我去仔细看了一遍proposal,发现确实支持数组。
尝试用了一下这个plugin,需要注意的是:需要babel 7才能解析这种语法,而babel 7还在alpha阶段:Planning for 7.0。简单地用一下babel-cli还可以,在一个实际的webpack项目里强行使用babel-core@next,结果编译无法通过。想要正式地使用这个语法还要等一段时间。
方法二:用封装好的函数解析字符串
lodash里提供了一个函数_.get,传送门:Lodash Documentation。
用起来如下:
import { get } from 'lodash'
const MyComponent = props => (
<div>
{get(props, 'user.info.address[0].city', '暂无城市信息')}
</div>
)
能解决问题,支持[ ],但是用字符串描述总觉得很不优雅,比方说里面有点动态内容:
import { get } from 'lodash'
const MyComponent = props => (
<div>
{get(props, `user.info.address[${props.index}].city`, '暂无城市信息')}
</div>
)
看起来又很不优雅了。
方法三:利用Proxy
这是我自己实现的一种方法,先介绍用会有什么样的使用效果:
import pointer from './pointer'
const MyComponent = props => (
<div>
{pointer(props).user.info.address[0].city('暂无城市信息')}
</div>
)
支持方括号,不拼字符串,唯一看着有点多余的是开头的pointer(...),不过这已经比较接近我心目中的优雅了。
缺点也是有的,这种方法依赖Proxy,目前的浏览器覆盖还不够好,而且还无法完美polyfill,存在的polyfill无法在这里起到效果。但如果只考虑先进的浏览器,那么这是目前我最喜欢的方案了。
Proxy是ES6规范中的一个对象类型,可以“劫持”对象的各种操作,比方说你可以在浏览器(当然要先进的浏览器)的console里试着运行以下代码:
let a = { x: 1 };
let b = new Proxy(a, {
get (target, key) {
return a[key] || 'Get away, nothing here!!';
}
});
console.log(b.x); // 1
console.log(b.y); // Get away, nothing here!!
console.log(b.z); // Get away, nothing here!!
利用这种特性可以通过劫持get行为在取值的时候根据情况返回东西,那么我只需要永远不返回undefined或null,就不会在取值的时候报错。
以下是我的实现:
// pointer.js
const dummy = () => {}
let G
(function () {
G = this
})() // 取得global或window,兼容Node与浏览器环境
function softBind (func, context) {
return function (...args) {
if (this === G) {
func.call(context, ...args)
} else {
console.log(context);
func.call(this, ...args)
}
}
}
function pointer (root, path = []) {
return new Proxy(dummy, {
get (target, property) {
return pointer(root, path.concat(property))
},
apply (target, self, args) {
let val = root
let parent
for (let i = 0; i < path.length; i++) {
if (val === null || val === undefined) {
break
}
parent = val
val = val[path[i]]
}
if (typeof val === 'function') {
val = softBind(val, parent)
}
if (val === null || val === undefined) {
val = args[0]
}
return val
}
})
}
export default pointer
本质思路是pointer返回的Proxy在取值的时候会再返回一个pointer,每一个pointer只记录了“根对象”以及“取值路径”,只有在最后当做函数调用的时候才会根据二者获取实际的内容,中途遇到会报错的null或者undefined就直接返回默认值。
这段代码里有个softBind可能会显得比较令人费解,这主要是为了支持这种用法:
pointer(obj).field.myMethod()()
即被取值的内容是个函数,而且会被立刻调用,这种情况下如果不做专门处理this会错误(函数执行时浏览器里this成了window而不是期望中的obj.field)。这里可以简单地用bind来解决,但是我觉得可能会造成问题(比如用户取到值之后自己bind或者调用apply,指定其他的东西作为this),所以我会加个判断,只有当函数执行发现this是global或者window的时候再使用取值来源作为this。