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

Jul 13, 2021

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

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 标签中使用

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.