前言

你是否也经常被下面这种报错整得抓狂?

  • “TypeError: Cannot read property *** of undefined”
  • “TypeError: Cannot read property *** of null”

通常这种报错,在控制台打印的都是堆栈信息,往往很难定位到具体位置,而且这种报错信息是很容易复发的,所以我们要经常脑神费心的去一次又一次的解决这个问题,这是一个前端通病大家都会遇到,今天这篇文章的主题就是找一个方法来完美解决这个问题!

分析原因

先看一段代码:

1
const firstName = message.body.user.firstName;
  • 假设 message 下面并没有 body,那么就会报错: Cannot read property user of undefined
  • 假设 message 下面有 body 但是值为 null,那么就会报错: Cannot read property user of null
  • 同理假设 meseage.body 没问题,但是 user 没有定义或者值为 null 会报错 Cannot read property firstName of undefined/null

日常开发中,前端通过接口获取后端数据后,往往都会出现这样的赋值,而后端如果不严格按照报文来返回数据结构,就会出现这种问题!

比如: 后端觉得 user 下面只有 firstName 一个属性,如果 firstName 有值,那就完整返回,如果没有值,直接连 user 这一层都去掉不返回。

  • firstName 有值时返回数据
1
2
3
4
5
6
7
{
"body": {
"user": {
"firstName": "Liu"
}
}
}
  • firstName 没有值时返回数据
1
2
3
{
"body": {}
}

传统解决方案

所以我们如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。安全的写法是写成下面这样。

1
2
3
4
5
// 正确的写法
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';

上面例子中,firstName 属性在对象的第四层,所以需要判断四次,每一层是否有值,非常的繁琐且有失优雅,而且经常是在前后端联调的时候是有值的,我们就直接赋值漏了判断也没问题(很容易忘记写判断,这也是为什么这个问题很容易复发的原因),但是等到上线之后,数据变成没有值了,后端返回的数据结构变动了,所以导致报错,而且还不容易定位具体位置,很难排查,所以我们希望后端就算没有值,也保留数据结构,返回完整的报文结构, 没有值就约定一个 空字符串 或者 null,这样就不需要一层一层去判断,比如这样:

1
2
3
4
5
6
7
{
"body": {
"user": {
"firstName": ""
}
}
}

链判断运算符

上面两种解决方案,第一种层层判断非常麻烦、不够优雅、容易遗忘,第二种后端报文基于框架原因或者个人习惯原因,数据结构也不可控,即使可控也麻烦且不安全,因此 ES2020 引入了 链判断运算符(optional chaining operator)?.,简化上面的写法。

1
const firstName = message?.body?.user?.firstName

上面代码使用了 ?. 运算符,直接在链式调用的时候判断,左侧的对象是否为 nullundefined。如果是的,就不再往下运算 (短路机制),而是返回 undefined。下面是判断对象方法是否存在,如果存在就立即执行的例子。

1
iterator.return?.()

上面代码中,iterator.return 如果有定义,就会调用该方法,否则 iterator.return 直接返回 undefined,不再执行 ?. 后面的部分。

如何在 vue 项目中应用这种新语法

vue 3.0 后可以直接使用 链判断运算符(?.) 语法, 否则就要借助 babel 插件解析帮助我们进行代码转换。

由于 vue 3.0 还没有普及,我们还是有必要研究一下如何使用插件提供语法支持:

升级 babel

vue-cli 搭建的项目中,babel 版本默认是小于 7 的,为了使用 @babel/plugin-proposal-optional-chaining, 我们需要先将 babel 升级到 7,官方提供了一个工具 babel-upgrade, 对于已有项目,只需要运行一行命令就可以升级到 7 版本以上了,如果找不到 npx 命令就检查一下 npm 版本是否 < 5.2.0.

1
npx babel-upgrade --write --install  

兼容版本

升级完之后运行项目可能会出现一些报错信息:

  • Cannot find module ‘babel-plugin-syntax-jsx’
  • Cannot find module ‘@babel/core’

根据报错信息,我们稍作调整,将 @babel/plugin-syntax-jsx 卸载,更换成 babel-plugin-syntax-jsx,将 @babel/polyfill 卸载,更换成 babel-polyfill,再安装一个 @babel/runtime-corejs2

1
2
3
4
5
6
7
npm uninstall @babel/plugin-syntax-jsx 
npm install babel-plugin-syntax-jsx --save-dev

npm uninstall @babel/polyfill
npm install --save babel-polyfill

npm install --save @babel/runtime-corejs2 -dev

修改配置文件

最后在 .babelrc 文件中注入插件, 在 plugins 数组中 插入 "@babel/plugin-proposal-optional-chaining":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
}
]
],
"plugins": [
"transform-vue-jsx",
[
"@babel/plugin-transform-runtime",
{
"corejs": 2
}
],
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-json-strings",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
"@babel/plugin-proposal-optional-chaining"
]
}

总结

完成上述步骤之后,不要忘记重启项目,通常配置文件有改动都需要重启项目才生效,然后就可以愉快的使用 链判断运算符(?.) 语法了,正式告别那些一层一层的判断,还有反复出现的 "TypeError: Cannot read property *** of undefined/null"