如何优雅地链式取值

2018/12/02

开发中, 链式取值是非常正常的操作, 如:

res.data.goods.list[0].price

但是对于这种操作报出类似于 Uncaught TypeError: Cannot read property 'goods' of undefined 这种错误也是再正常不过了, 如果说是 res 数据是自己定义, 那么可控性会大一些, 但是如果这些数据来自于不同端(如前后端), 那么这种数据对于我们来说我们都是不可控的, 因此为了保证程序能够正常运行下去, 我们需要对此校验:

if (res.data.goods.list[0] && res.data.goods.list[0].price) {
  // your code
}

如果再精细一点, 对于所有都进行校验的话, 就会像这样:

if (
  res &&
  res.data &&
  res.data.goods &&
  res.data.goods.list &&
  res.data.goods.list[0] &&
  res.data.goods.list[0].price
) {
  // your code
}

不敢想象, 如果数据的层级再深一点会怎样, 这种实现实在是非常不优雅, 那么如果优雅地来实现链式取值呢?

一、 optional chaining

这是一个出于 stage 2 的 ecma 新语法, 目前已经有了 babel 的插件 babel-plugin-transform-optional-chaining, 这种语法在 swift 中有, 可以看下官方给的实例

a ? .b // undefined if `a` is null/undefined, `a.b` otherwise.
a == null ? undefined : a.b

a ? . [x] // undefined if `a` is null/undefined, `a[x]` otherwise.
a == null ? undefined : a[x]

a ? .b() // undefined if `a` is null/undefined
a == null ? undefined : a.b() // throws a TypeError if `a.b` is not a function
// otherwise, evaluates to `a.b()`

a ? .() // undefined if `a` is null/undefined
a == null ? undefined : a() // throws a TypeError if `a` is neither null/undefined, nor a function
// invokes the function `a` otherwise

二、 通过函数解析字符串

我们可以通过函数解析字符串来解决这个问题, 这种实现就是 lodash 的 _.get 方法

var object = {
  a: [
    {
      b: {
        c: 3
      }
    }
  ]
}
var result = _.get(object, 'a[0].b.c', 1)
console.log(result)
// output: 3

这里还有一些其他的库, 如 TypyRamda , 可以做到这一点. 但是在轻量级前端项目中, 特别是如果你只需要这些库中的一两个方法时, 最好选择另一个轻量级库, 或者编写自己的库.

实现起来也非常简单, 只是简单的字符串解析而已:

function get(obj, props, def) {
  if (obj == null || typeof props !== 'string') return def
  const temp = props.split('.')
  const fieldArr = [].concat(temp)
  temp.forEach((e, i) => {
    if (/^(\w+)\[(\w+)\]$/.test(e)) {
      const matchs = e.match(/^(\w+)\[(\w+)\]$/)
      const field1 = matchs[1]
      const field2 = matchs[2]
      const index = fieldArr.indexOf(e)
      fieldArr.splice(index, 1, field1, field2)
    }
  })
  return fieldArr.reduce((pre, cur) => {
    const target = pre[cur] || def
    if (target instanceof Array) {
      return [].concat(target)
    }
    if (target instanceof Object) {
      return Object.assign({}, target)
    }
    return target
  }, obj)
}
var c = {
  a: {
    b: [1, 2, 3]
  }
}
get(c, 'a.b') // [1,2,3]
get(c, 'a.b[1]') // 2
get(c, 'a.d', 12) // 12

三、 使用解构赋值

这个思路是来自 github 上 You-Dont-Need-Lodash-Underscore 这个仓库, 看到这个的时候真的佩服

const c = {
  a: {
    b: [1, 2, 3, 4]
  }
}

const { a: result } = c
// result : {b: [1,2,3,4]}
const {
  a: { c: result = 12 }
} = c
// result: 12

当然, 这个时候为了保证不报 uncaught TypeError, 我们仍然需要定义默认值, 就像这样, 貌似如果不加 lint 可读性堪忧

const {
  a: { c: { d: result2 } = {} }
} = c

四、 使用 Proxy

这个是组内同事提到的, 一个简单实现如下:

function pointer(obj, path = []) {
  return new Proxy(() => {}, {
    get(target, property) {
      return pointer(obj, path.concat(property))
    },
    apply(target, self, args) {
      let val = obj
      let parent
      for (let i = 0; i < path.length; i++) {
        if (val === null || val === undefined) break
        parent = val
        val = val[path[i]]
      }
      if (val === null || val === undefined) {
        val = args[0]
      }
      return val
    }
  })
}

我们可以这样使用:

let c = {
  a: {
    b: [1, , 2, 3]
  }
}

pointer(c).a() // {b: [1,2,3]}

pointer(c).a.b() // [1,2,3]

pointer(d).a.b.d('default value') // default value

五、 Oliver Steele 的嵌套对象访问模式

这是我个人的最爱, 因为它使代码看起来干净简单. 我从 stackoverflow 中选择了这种风格, 一旦你理解它是如何工作的, 它就非常吸引人了.

const name = ((user || {}).personalInfo || {}).name

使用这种表示法, 永远不会遇到无法读取未定义的属性 name . 做法是检查用户是否存在, 如果不存在, 就创建一个空对象, 这样, 下一个级别的键将始终从存在的对象访问.

不幸的是, 你不能使用此技巧访问嵌套数组.

六、 使用数组 Reduce 访问嵌套对象

Array reduce 方法非常强大, 可用于安全地访问嵌套对象.

const getNestedObject = (nestedObj, pathArr) => {
  return pathArr.reduce(
    (obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : null),
    nestedObj
  )
}

// 将对象结构作为数组元素传入
const name = getNestedObject(user, ['personalInfo', 'name'])

// 要访问嵌套数组,只需将数组索引作为数组元素传入。.
const city = getNestedObject(user, ['personalInfo', 'addresses', 0, 'city'])
// 这将从 addresses 中的第一层返回 city

这差不多就是心中所谓的优雅了.

综上, 在实际工作中, 使用方法四会是最优雅, 可读性也非常强, 但考虑到浏览器的话, 可能方法二会更加常用, 当然, 如果你所要取的值层级不是太深, 你组内的同事要严格的 lint, 方法三也不失为一种好的选择.

参考资料