在快应用中链式取值的正确姿势

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

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
}

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

方法一:通过函数解析字符串(lodash 的 _.get 方法)

首先,我们需要安装 lodash

npm install lodash --save

然后就可以在 ux 文件中使用_.get 方法了

<template>
  <div class="wrapper">
    <!-- grandson -->
    <text>{{
      _get(this, "parent.children[0].grandson[0].name", "default")
    }}</text>
    <!-- default -->
    <text>{{
      _get(this, "parent.non_children[0].grandson[0].name", "default")
    }}</text>
  </div>
</template>

<script>
import _ from "lodash";

export default {
  data: {
    parent: {
      name: "parent",
      children: [
        {
          name: "children",
          grandson: [
            {
              name: "grandson",
            },
          ],
        },
      ],
    },
  },
  onInit() {
    console.log(
      "use lodash",
      _.get(this, "parent.children[0].grandson[0].name", "default")
    ); //grandson
    console.log(
      "use lodash",
      _.get(this, "parent.non_children[0].grandson[0].name", "default")
    ); //default
  },
  //混入_.get方法,使_.get能够在template标签中使用
  _get: _.get,
};
</script>

除了直接使用 lodash 的_.get 方法,我们也可以自己封装一个函数来解析字符串

/**
 * get.js
 */
export function _get(obj, props, def) {
  if (obj == default || obj == null || typeof props !== 'string') return def;
  const temp = props.split('.');
  const fieldArr = [].concat(temp);
  temp.forEach((e, i) => {
    if (/^(\w+)\[(\w+)\]$/.test(e)) {
      //解析obj[xxx]
      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) => {
    if (pre === default) return pre
    const target = pre[cur] || def;
    if (target instanceof Array) {
      return [].concat(target);
    }
    if (target instanceof Object) {
      return Object.assign({}, target)
    }
    return target;
  }, obj)
}

然后就可以直接在 ux 文件中使用了

<template>
  <div class="wrapper">
    <!-- grandson -->
    <text>{{
      _get(this, "parent.children[0].grandson[0].name", "default")
    }}</text>
    <!-- default -->
    <text>{{
      _get(this, "parent.non_children[0].grandson[0].name", "default")
    }}</text>
  </div>
</template>

<script>
import { _get } from "path to get.js";

export default {
  private: {
    parent: {
      name: "parent",
      children: [
        {
          name: "children",
          grandson: [
            {
              name: "grandson",
            },
          ],
        },
      ],
    },
  },

  onInit() {
    console.log(
      "use get.js file",
      _get(this, "parent.children[0].grandson[0].name", "default")
    ); //grandson
    console.log(
      "use get.js file",
      _get(this, "parent.non_children[0].grandson[0].name", "default")
    ); //default
  },
  //混入_get方法,使_get能够在template标签中使用
  _get,
};
</script>

方法二:使用 Proxy

除了通过解析字符串的方式读取属性,也可以将源对象加一层代理,在 handler 中对读取操作做一些处理

/**
 * get.js
 */
export function _get(obj, path = []) {
  return new Proxy(() => { }, {
    get(target, property) {
      return _get(obj, path.concat(property))
    },
    apply(target, self, args) {
      let val = obj;
      for (let i = 0; i < path.length; i++) {
        if (val === null || val === default) break;
        val = val[path[i]]
      }
      if (val === null || val === default) {
        val = args[0]
      }
      return val;
    }
  })
}
<template>
  <div class="wrapper">
    <!-- grandson -->
    <text>{{ _get(parent).children[0].grandson[0].name("default") }}</text>
    <!-- default -->
    <text>{{ _get(parent).non_children[0].grandson[0].name("default") }}</text>
  </div>
</template>

<script>
import { _get } from "path to get.js";

export default {
  private: {
    parent: {
      name: "parent",
      children: [
        {
          name: "children",
          grandson: [
            {
              name: "grandson",
            },
          ],
        },
      ],
    },
  },

  onInit() {
    console.log(
      "use proxy",
      _get(this.parent).children[0].grandson[0].name("default")
    ); //grandson
    console.log(
      "use proxy",
      _get(this.parent).non_children[0].grandson[0].name("default")
    ); //default
  },
  //混入_get方法,使_get能够在template标签中使用
  _get,
};
</script>

注意,不管是否传入默认值,末尾都要加上()作为函数调用,以触发apply捕捉器

方法三:可选链

第三种方式是使用 ES 的新语法,这种方式需要借助 babel 来使用。首先检查你的项目依赖中的babel版本,如果你的 babel 版本<7,那么得先解决 babel 版本升级的问题。如果是 babel7 以上的版本,可以添加以下devDependencies依赖:

@babel/plugin-proposal-optional-chaining

然后在.babelrc 或者 babel.config.js 中这加入这个插件:

{
  "plugins": ["@babel/plugin-proposal-optional-chaining"]
}

之后就可以愉快地使用了!

<template>
  <div class="wrapper">
    <!-- grandson -->
    <text>{{ parent?.children?.[0]?.grandson?.[0]?.name || "default" }}</text>
    <!-- default -->
    <text>{{
      parent?.non_children?.[0]?.grandson?.[0]?.name || "default"
    }}</text>
  </div>
</template>

<script>
export default {
  private: {
    parent: {
      name: "parent",
      children: [
        {
          name: "children",
          grandson: [
            {
              name: "grandson",
            },
          ],
        },
      ],
    },
  },

  onInit() {
    console.log(
      "use optional chaining",
      this.parent?.children?.[0]?.grandson?.[0]?.name || "default"
    ); //grandson
    console.log(
      "use optional chaining",
      this.parent?.non_children?.[0]?.grandson?.[0]?.name || "default"
    ); //default
  },
};
</script>

另外,如果使用的是最新版本的快应用开发工具,可以无须配置直接使用可选链的语法

注意:可选链可以直接在 template 标签中使用