ES 提案:可选链(optional chaining)

这篇博客文章讲述由 Gabriel Isenberg, Claude Pache, 和 Dustin Savery 提出的 ECMAScript 提案 “Optional chaining”

概述

存在下面种类的可选操作。

obj?.prop     // optional static property access
obj?.[«expr»] // optional dynamic property access
func?.(«arg0», «arg1») // optional function or method call

大概的意思是:

  • 如果问号前面的值不是 undefinednull,那么运行问号后面的操作。
  • 其它情况,返回 undefined

例子:访问可选静态属性

考虑下面的数据:

const persons = [
  {
    surname: 'Zoe',
    address: {
      street: {
        name: 'Sesame Street',
        number: '123',
      },
    },
  },
  {
    surname: 'Mariner',
  },
  {
    surname: 'Carmen',
    address: {},
  },
];

我们可以使用可选链安全取出 street 中的 name:

const streetNames = persons.map((p) => p.address?.street?.name);

console.log(streetNames);
// => ['Sesame Street', undefined, undefined]

通过合并空值(nullish coalescing)处理默认值

合并空值操作符允许我们使用默认值 '(no street)' 替代 undefined

const streetNames = persons.map((p) => p.address?.street?.name ?? '(no name)');

console.log(streetNames);
// => ['Sesame Street', '(no name)', '(no name)']

高级特性

接下来章节介绍可选链的高级部分。

该操作符的更多详情

访问可选的静态属性

下面的两个表达式是等价的:

o?.prop;

o !== undefined && o !== null ? o.prop : undefined;

例子:

console.log(undefined?.prop); // undefined
console.log(null?.prop); // undefined
console.log({ prop: 1 }?.prop); // 1

访问动态属性

下面的两个表达式是等价的:

o?.[«expr»];

(o !== undefined && o !== null) ? o[«expr»] : undefined;

例子:

const key = 'prop';

console.log(undefined?.[key]); // undefined
console.log(null?.[key]); // undefined
console.log({ prop }?.[key]); // 1

执行可选的函数或方法

下面的两个表达式是等价的:

f?.(arg0, arg1);

f !== undefined && f !== null ? f(arg0, arg1) : undefined;

例子:

console.log(undefined?.(123)); // undefined
console.log(null?.(123)); // undefined
console.log(String?.(123)); // "123"

注意如果这个操作符的左侧不是可以执行的时候,它将产生错误:

true?.(123); // TypeError

为什么?这个想法是该操作符仅仅允许故意的疏漏。一个不能执行的值(undefinednull 除外)是一个可能的错误,且应该报道,而不是无视。

短路

在属性的访问链和函数/方法的调用,执行在第一个可选操作符左侧遇到 undefinednull 时停止:

function isInvoked(obj) {
  let invoked = false;
  obj?.a.b.m((invoked = true));
  return invoked;
}

console.log(isInvoked({ a: { b: { m() {} } } })); // true

// The left-hand side of ?. is undefined
// and the assignment is not executed
console.log(isInvoked(undefined)); // false

此行为不同于普通的操作符/函数,其中 JavaScript 在评估操作符/函数之前始终评估所有操作数/参数。这被称为短路。其它短路操作符:

  • a && b
  • a || b
  • c ? t : e

可选链的替代方案

直到现在,在 JavaScript 中我们使用下面的方法替代可选链。

&& 操作符

下面的两个表达式大致等价:

p.address?.street?.name;

p.address && p.address.street && p.address.street.name;

对于每个 a && bb 仅仅在如果 a 是真值(truthy[1])时被选中。a 作为 b 之前的条件或者守卫。

&& 操作符的问题

除了冗长以外,&& 还有两个问题。

首先,如果失败,&& 返回它左侧的值,然而 ?. 总是返回 undefined

const value = null;
console.log(value && value.prop); // null
console.log(value?.prop); // undefined

第二,&& 在左侧的值为假值(falsy[2])时都会失败,然而 ?. 仅仅在 undefinednull 时失败:

const value = '';
console.log(value?.length); // 0
console.log(value && value.length); // ""

注意这里,&& 返回它左侧的值是错误的相比之前的例子。

解构

原则上,你也可以使用解构操作属性访问链。但是不是很好:

for (const p of persons) {
  const { address: { street: { name = undefined } = {} } = {} } = p;

  console.log(name === p.address?.street?.name);
}

Lodash get()

库 lodash 的函数 get()是另外一个可选链的替代方案。

举例来说,下面两个表达式是等价的:

import { get } from 'lodash-es';

p.address?.street?.name;

get(p, 'address.street.name');

可选链的可用性

常见问题

为什么 o?.[x]f?.() 两者都包含点号

下面两种可选操作的语法不是理想的:

obj?.[«expr»]          // better: obj?[«expr»]
func?.(«arg0», «arg1») // better: func?(«arg0», «arg1»)

不太优雅的语法是必要的,因为区分理想语法(第一个表达式)和条件操作符(第二个表达式)太复杂了:

obj?['a', 'b', 'c'].map(x => x+x);
obj ? ['a', 'b', 'c'].map(x => x+x) : [];

为什么 null?.prop 评估为 undefined 而不是 null

操作符 ?. 主要关注是它的右侧:属性 .prop 是否存在?如果不存在,提前停止。因此,保持其左侧的信息很少有用。但,只有一个“提前终止”值确实简化了事情。

译者注
[1] truthy 不是 true,详见 MDN 的解释。
[2] falsy 不是 false,详见 MDN 的解释。

英文原文:https://2ality.com/2019/07/optional-chaining.html

Copyright © 2019 by Wu WenJun