无报错链式取值的几种方法

无报错链式取值的几种方法

先看下面这段代码:

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。

编辑于 2017-09-14 12:00