链判断运算符

编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取message.body.user.firstName,安全的写法是写成下面这样。

  1. const firstName = (message
  2. && message.body
  3. && message.body.user
  4. && message.body.user.firstName) || 'default';

或者使用三元运算符?:,判断一个对象是否存在。

  1. const fooInput = myForm.querySelector('input[name=foo]')
  2. const fooValue = fooInput ? fooInput.value : undefined

这样的层层判断非常麻烦,因此 ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。

  1. const firstName = message?.body?.user?.firstName || 'default';
  2. const fooValue = myForm.querySelector('input[name=foo]')?.value

上面代码使用了?.运算符,直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是的,就不再往下运算,而是返回undefined

链判断运算符有三种用法。

  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法的调用

下面是判断对象方法是否存在,如果存在就立即执行的例子。

  1. iterator.return?.()

上面代码中,iterator.return如果有定义,就会调用该方法,否则直接返回undefined

对于那些可能没有实现的方法,这个运算符尤其有用。

  1. if (myForm.checkValidity?.() === false) {
  2. // 表单校验失败
  3. return;
  4. }

上面代码中,老式浏览器的表单可能没有checkValidity这个方法,这时?.运算符就会返回undefined,判断语句就变成了undefined === false,所以就会跳过下面的代码。

下面是这个运算符常见的使用形式,以及不使用该运算符时的等价形式。

  1. a?.b
  2. // 等同于
  3. a == null ? undefined : a.b
  4. a?.[x]
  5. // 等同于
  6. a == null ? undefined : a[x]
  7. a?.b()
  8. // 等同于
  9. a == null ? undefined : a.b()
  10. a?.()
  11. // 等同于
  12. a == null ? undefined : a()

上面代码中,特别注意后两种形式,如果a?.b()里面的a.b不是函数,不可调用,那么a?.b()是会报错的。a?.()也是如此,如果a不是nullundefined,但也不是函数,那么a?.()会报错。

使用这个运算符,有几个注意点。

(1)短路机制

  1. a?.[++x]
  2. // 等同于
  3. a == null ? undefined : a[++x]

上面代码中,如果aundefinednull,那么x不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。

(2)delete 运算符

  1. delete a?.b
  2. // 等同于
  3. a == null ? undefined : delete a.b

上面代码中,如果aundefinednull,会直接返回undefined,而不会进行delete运算。

(3)括号的影响

如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。

  1. (a?.b).c
  2. // 等价于
  3. (a == null ? undefined : a.b).c

上面代码中,?.对圆括号外部没有影响,不管a对象是否存在,圆括号后面的.c总是会执行。

一般来说,使用?.运算符的场合,不应该使用圆括号。

(4)报错场合

以下写法是禁止的,会报错。

  1. // 构造函数
  2. new a?.()
  3. new a?.b()
  4. // 链判断运算符的右侧有模板字符串
  5. a?.`{b}`
  6. a?.b`{c}`
  7. // 链判断运算符的左侧是 super
  8. super?.()
  9. super?.foo
  10. // 链运算符用于赋值运算符左侧
  11. a?.b = c

(5)右侧不得为十进制数值

为了保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。