# 1. 引言

备受开发者喜爱的特性 [Optional chaining](https://github.com/tc39/proposal-optional-chaining) 在 2019.6.5 进入了 stage2，让我们详细读一下草案，了解一下这个特性的用法以及讨论要点。

借着这次精读草案，让我们了解一下一个完整草案的标准文档结构是怎样的。

一个新特性的文档，首先要描述 **起因** 是什么，也就是为什么要增加这个特性，大家不会没有理由的就增加一个特性。其次是**其他语言是否有现成的实现版本**，参考他们并进行归纳总结，可以增加思考角度的全面性。

第三点就是 **语法介绍**，也就进入了新特性的正题，这里要详细介绍所有可能的使用情况。第四点是 **语义**，也就是诠释语法的含义。

然后是可选的 **是否有不支持的情况**，对于不支持的点是否有意而为之，为什么？此处一般会留下讨论的 ISSUE。然后是 **暂不考虑的点**，是由于性价比低、使用场景少，或者实现成本高的原因，为什么某些已经想到的点暂不考虑，这里也会留下讨论的 ISSUE。

后面一般还有 “正在讨论的点”、“FAQ”、“草案进度”、“参考文献”、“相关问题”、“预先讨论资料” 等内容。

# 2. 概述&精读

首先让我们回顾一下什么是 **“Optional chaining”**。

## 起因介绍

当访问一个深层树形结构的对象时，我们总需要判断中间节点属性是否存在：

```js
var street = user.address && user.address.street;
```

而且很多 API 返回的属性都可能为 Null，而我们往往只想获取非 Null 时的结果：

```js
var fooInput = myForm.querySelector('input[name=foo]')
var fooValue = fooInput ? fooInput.value : undefined
```

> 笔者这里补充，在人机交互的领域，可能为 Null 的情况很多。首先是交互行为模块很多，行为复杂，很容易导致数据分散且难以预测（可能为空），仅是 DOM 元素就需要太多兼容，因为 DOM 被修改的实际太多了，大家都在共享一个可变的结构；其次是交互过程中间状态很多，出现状态残缺的可能性也很大，就拿 SQL 解析为例：后端只要检测 Query 是否正确就可以了，但前端的 SQL 编辑器需要在输入不完整的情况下给出提示，也就是在语法树错误的情况下给出提示，因此需要进行容错。

而 Optional chaining 可以解决为了容错而写过多重复代码的问题：

```js
var street = user.address?.street
var fooValue = myForm.querySelector('input[name=foo]')?.value
```

正如上面的例子：如果 `user.address` 为 `undefined`，那 `street` 拿到的就是 `undefined`，而不是报错。

配合另一个在 stage2 的新特性 [Nullish Coalescing](https://github.com/tc39/proposal-nullish-coalescing) 做默认值处理非常方便：

```js
// falls back to a default value when response.setting is missing or nullish
// (response.settings == null) or when respsonse.setting.animationDuration is missing
// or nullish (response.settings.animationDuration == null)
const animationDuration = response.settings?.animationDuration ?? 300;
```

`??` 号可以理解为 “默认值场景下的 `||`”：

```js
const response = {
  settings: {
    nullValue: null,
    height: 400,
    animationDuration: 0,
    headerText: '',
    showSplashScreen: false
  }
};

const undefinedValue = response.settings?.undefinedValue ?? 'some other default'; // result: 'some other default'
const nullValue = response.settings?.nullValue ?? 'some other default'; // result: 'some other default'
const headerText = response.settings?.headerText ?? 'Hello, world!'; // result: ''
const animationDuration = response.settings?.animationDuration ?? 300; // result: 0
const showSplashScreen = response.settings?.showSplashScreen ?? true; // result: false
```

`0 || 1` 的结果是 `1`，因为 `0` 判定为 `false`，而 `||` 在前面的变量为 `false` 型才继续执行，而我们想要的是 “前面的对象不存在时才使用后面的值”。`??` 则代表了 “前面的对象不存在” 这个含义，即便值为 `0` 也会认为这个值是存在的。

Optional chaining 也可以用在方法上：

```js
iterator.return?.()
```

或者试图调用某些未被实现的方法：

```js
if (myForm.checkValidity?.() === false) { // skip the test in older web browsers
    // form validation fails
    return;
}
```

比如某个旧版本浏览器不支持 `myForm.checkValidity` 方法，则不会报错，而是返回 `false`。

## 已有实现调研

Optional chaining 在 C#、Swift、CoffeeScript、Kotlin、Dart、Ruby、Groovy 已经实现了，且实现方式均有差异，可以看到每个语言在实现语法时都是有取舍的，但是大方向基本是相同的。

想了解其他语言是如何实现 Optional chaining 的读者可以 [点击阅读原文](https://github.com/tc39/proposal-optional-chaining#prior-art)。

这些语言实现 Optional chaining 的差异基本在 **语法、支持范围、边界情况处理** 等不同，所以如果你每天要在不同语言之间切换工作，看似相同的语法，但不同的细节可能把你绕晕（所以会的语言多，只会让你变成一个速记字典，满脑子都是哪些语言在哪些语法讨论倾向哪一边，选择了哪些特性这些毫无意义的结论，如果不想记这些，基础语法都没有掌握怎么好意思说会这门语言呢？所以学 JS 就够了）。

## 语法

Optional Chaining 的语法有三种使用场景：

```js
obj?.prop       // optional static property access
obj?.[expr]     // optional dynamic property access
func?.(...args) // optional function or method call
```

也就是将 `.` 替换为 `?.`，但要注意第二行与第三行稍稍有点反直觉，比如在函数调用时，需要将 `func(...args)` 写为 `func?.(...args)`。至于为什么语法不是 `func?(...args)` 这种简洁一点的表达方式，在 FAQ 中有提到这个例子：

`obj?[expr].filter(fun):0` 引擎难以判断 `obj?[expr]` 是 Optional Chaning，亦或这是一个普通的三元运算语句。

可见，要支持 `?.` 这个看似简单的语法，在整个 JS 语法体系中要考虑的边界情况很多。

即便是 `?.` 这样完整的用法，也需要注意 `foo?.3:0` 这种情况，不能将 `foo?.` 解析为 Optional chanining，而要将其解析为 `foo? .3 : 0`，这需要解析引擎支持 lookahead 特性。

## 语义

**当 `?.` 前面的变量值为 `null` 或 `undefined` 时，`?.` 返回的结果为 `undefined`**。

```js
a?.b                          // undefined if `a` is null/undefined, `a.b` otherwise.
a == null ? undefined : a.b

a?.[x]                        // undefined if `a` is null/undefined, `a[x]` otherwise.
a == null ? undefined : a[x]

a?.b()                        // undefined if `a` is null/undefined
a == null ? undefined : a.b() // throws a TypeError if `a.b` is not a function
                              // otherwise, evaluates to `a.b()`

a?.()                        // undefined if `a` is null/undefined
a == null ? undefined : a()  // throws a TypeError if `a` is neither null/undefined, nor a function
                             // invokes the function `a` otherwise
```

### 短路

所谓短路，就是指引入了 Optional chaining 后，某些看似一定会执行的语句在特定情况下会短路（终止执行），比如：

```js
a?.[++x]         // `x` is incremented if and only if `a` is not null/undefined
a == null ? undefined : a[++x]
```

第一个例子，如果 `a` 时 `null/undefined`，就不会执行 `++x`。

原因是这段代码部分等价于 `a == null ? undefined : a[++x]`，如果 `a == null` 为真，自然不会执行 `a[++x]` 这个语句。但由于 Optional chaining 使这个语句变得 “简洁了”，虽然带来了便利，但也可能导致看不清完整的执行逻辑，引发误判。

所以看到 `?.` 语句时，一定要反射性的思考一下，这个语句会触发 “短路”。

### 长“短路”

Optional chaining 在 JS 的规范中，作用域仅限于调用处。看下面的例子：

```js
a?.b.c(++x).d  // if `a` is null/undefined, evaluates to undefined. Variable `x` is not incremented.
               // otherwise, evaluates to `a.b.c(++x).d`.
a == null ? undefined : a.b.c(++x).d
```

可以看到 `?.` 仅在 `a?.` 这一层生效，而不是对后续的 `b.c`、`c(++x).d` 继续生效。而对于 C+ 与 CoffeeScript，这个语法是对后续所有 `get` 生效的（**这里再次提醒，不要用 `CoffeeScript` 了，因为对于相同语法，语义都发生了变化，对你与你的同事都是巨大的理解负担，或者说没有人愿意注意，为什么代码在 `CoffeeScript` 里不报错，而转移到 JS 就报错了，是因为 Optional chaining 语义不一致造成的。**）。

正因为 Optional chaining 在 JS 语法中仅对当前位置起保护作用，因此一个调用语句中允许出现多个 `?.` 调用：

```js
a?.b[3].c?.(x).d
a == null ? undefined : a.b[3].c == null ? undefined : a.b[3].c(x).d
  // (as always, except that `a` and `a.b[3].c` are evaluated only once)
```

上面这段代码，对 `a?.b`、`c?.(x)` 的访问与调用是安全的，而对于 `b[3]`、 `b[3].c`、`c?.(x).d` 的调用是不安全的。

在 FAQ 环节也提到了，为什么不学习 C# 与 CoffeeScript 的语义，将安全保护从 `a?.` 之后就一路 “贯穿” 下去？

原因是 JS 对 Optional chaining 的理解不同导致的。Optional chaining 仅仅是安全访问保护，不代表 `try catch`，也就是它不会捕获异常，举一个例子：

```js
a?.b()
```

这个调用，在 `a.b` 不是一个函数时依然会报错，原因就是 Optional chaining 仅提供了对属性访问的安全保护，不代表对整个执行过程进行安全保护，该抛出异常还是会抛出异常，因此 Optional chaining 没有必要对后面的属性访问安全性负责。

笔者认为 TC39 对这个属性的理解是合理的，否则用 `try catch` 就能代替 Optional chaining 了。**让一个特性仅实现分内的功能，是每个前端从业者都要具备的思维能力。**

> PS：笔者再多提一句，在任何技术设计领域，这个概念都适用。想想你设计的功能，写过的函数，如果为了图方便，扩大了其功能，终究会带来整体设计的混乱，适得其反。

### 边界情况 - 分组

我们知道，JS 代码可以通过括号的方式进行分组，分组内的代码拥有更高的执行优先级。那么在 Optional chaining 场景下考虑这个情况：

```js
(a?.b).c
(a == null ? undefined : a.b).c
```

与不带括号的进行对比：

```js
a?.b.c
a == null ? undefined : a.b.c
```

我们会发现，由于括号提高了优先级，导致在 `a` 为 `null/undefined` 时，解析出了 `undefined.c` 这个必定报错的荒谬语法。因此我们不要试图为 Optional chaining 进行括号分组，这样会打破逻辑顺序，使安全保护不但不生效，反而导致报错。

### Optional delete

中文大概可以翻译为 “安全删除” 吧，也就是 JS 的 Optional chaining 支持下面的使用方式：

```js
delete a?.b
a == null ? true : delete a.b
```

这样不论 `b` 是否存在，得到的都是 `b` 删除成功的信号（返回值 `true`）。

至于为什么要支持 Optional delete，草案里也有提到，笔者认为非常有意思：

讨论重点应该是 “我们为什么不支持 Optional delete”，而不是 “我们为什么要支持 Optional delete”，有点像反证法的思路。由于 Optional delete 具备一定的使用场景，而且支持方式零成本（改写为 `a == null ? true : delete a.b` 即可），所以就支持它吧！

## 不支持的特性

下面三个特性不支持，原因是没什么使用场景：

- 安全的 construction：`new a?.()`
- 安全的 template literal：a?.\`string\`
- 上面两者的结合：`new a?.b()`, a?.b\`string\`

首先看 new 一个对象，如果 new 出来的结果是 `undefined`，那这个返回值使用起来也没有意义。

对于第二个安全的 template literal 来说，比如下面的语法：

```js
a?.b
`c`
```

会被解析为

```js
a == null ? undefined : a.b`c`
```

那么对于下面这种翻译结果：

```js
a == null ? undefined : a.b `c`
```

目前不会有人这么写代码，因为这种语法的使用场景一般都是 “前面的属性必定存在时的简化语法”，比如 `styled-components` 的：

```js
div`
  width: 300px;
`
```

而如果解析为：

```js
(a == null ? undefined : a?.b) `c`
```

则更不会有人愿意尝试这种写法，所以安全的 template literal 这种需求是不存在的，自然第三种需求也是不存在的。

下面一个不支持的特性，虽然有一定使用场景，但依然被否定的：

- 安全的赋值：`a?.b = c`

[讨论 ISSUE](https://github.com/tc39/proposal-optional-chaining/issues/18)

笔者总结一下，一共有这几种令人烦恼的地方，导致大家不想支持 **安全赋值** 特性：

**短路特性导致的理解成本：**

比如 `a?.b = c()`，如果 `a` 为 `null/undefined`，那么函数 `c()` 就不会被执行，这种语法太违背开发者的常识，如果支持这个特性带来的理解负担会很大。

**连带考虑场景很多：**

如果支持了这种看似简单的赋值场景，那么至少还有下面五种赋值场景需要考虑到：

- 简单赋值: `a?.b = c`
- 聚合赋值: `a?.b += c, a?.b >>= c`
- 自增，自减: `a?.b++, --a?.b`
- 解构赋值: `{ x: a?.b } = c, [ a?.b ] = c`
- for 循环中的临时赋值: `for (a?.b in c), for (a?.b of c)`

总和这几种考虑，支持安全赋值会带来更多灵活的用法，导致代码复杂度陡增（想想你的同事大量使用上面的后四种例子，你绝对想要找他决斗，因为这种写法和乱用 window 变量一样，在 JS 允许的框架内写出难以维护的逻辑，像是钻了法律的孔子），因此 TC39 决定不支持这种用法，从源头上杜绝被滥用。

以上不支持的功能点会在静态编译时被禁止，但以后也许会重新讨论。

另外对于 Class 的私有变量是否支持 `a?.#b` `a?.#b()` 还在讨论中，这取决于私有成员变量草案是否能最终落地。

## 暂不讨论的点

目前有两个 Optional chaining 功能点暂不讨论，分别是 [Optional spread](https://github.com/tc39/proposal-optional-chaining/issues/55) 与 [Optional destructuring](https://github.com/tc39/proposal-optional-chaining/issues/74)

对于 Optional spread，建议是：

```js
const arr = [...?listOne, ...?listTwo];
foo(...?args);
```

但由于可以结合 [Nullish Coalescing](https://github.com/tc39/proposal-nullish-coalescing) 达到同样的效果：

```js
foo(...args ?? [])
```

所以暂时不深入讨论，因为存在意义不大。

对于 Optional destructuring，建议是：


```js
// const baz = obj?.foo?.bar?.baz; 
const { baz } = obj?.foo?.bar?;
```

也就是对于解构用法，在最后一个位置添加 `?`，使其能安全的解构。

但由于基于这个特性会演变出太多的使用变体：

```js
‪const {foo ?: {bar ?: {baz}}} = obj?
```

或者 

```js
const {
  foo?: {
    bar?: { baz }
  }
} = obj;
```

对开发者的理解成本压力较大，毕竟 Optional chaining 的出发点只是 `?.` 这么简单。而且对于默认值，我们又有 `??` 语法可以快速满足，因此这个特性的讨论也被搁置了。

## 余下的 Q&A

大部分 Q&A 在上面的解读都有提及，下面列出剩余的两个 Q&A：

### 为什么语法是 `?.` 而不是 `.?` ?

原因是与三元运算符冲突了，思考下面的用法：

```js
1.?foo : bar
```

在 js 中，`1.` 等价于 `1`，那么这就是一个标准的三元运算表达式，因此 `.?` 语法会产生歧义，只能选择 `?.`。

### 为什么 `null?.b` 的结果不是 `null` 呢？

由于 `.` 表达式不关心 `.` 前面对象的类型，因为它的目的是访问 `.` 后面的属性，因此不会因为 `null?.b` 就返回 `null`，而是统一返回 `undefined`。

最后，需要 TC39 最终审核后，Optional chaining 才能进入 Stage3，我们拭目以待吧！

# 3. 总结

写一篇 JS 特性草案的完整解读真的很累，以后也许很少有机会这么完整的解读草案了，但希望借着这次解读 Optional chaining 的机会，让大家理解 TC39 是如何制定草案的，草案都在讨论什么，怎么讨论的，流程有哪些。

同时，还希望让大家意识到，为一个语言添加一个看似简单的新特性有多么的不容易，一个简单的 `?.` 语法就牵涉到与三元运算符、分组、解构等等已存在语法的交织与冲突，所以想要安全又妥当的添加一个新特性，参与讨论的人必须对 JS 语言有完整全面的理解，同时也要对边界情况考虑的很周全，懂得对语法融会贯通。

最后，希望大家可以意识到，JS 这么重量级的语言，一个新的语法特性其实也是这么三言两语讨论下来的，其中不乏有一些拍脑袋的地方、对于“即可也可”的情况，稍稍结合一些具体案例就定下来其中一种的现象也是存在的，甚至对于某些规范点根本不存在一个完美的 “真理”，比如为什么语法是 `?.` 而不是 `a&.b`（Ruby 使用的就是 `&.`），认清了这种情况存在，就不会执着于 “语法的学习”，而转向更底层，更有用的 “语义的学习”，并能通过阅读 TC39 的草案了解其他语言的实现差异，从而快速掌握其他语言的语法。

> 讨论地址是：[精读《Optional chaining》 · Issue #165 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/165)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

**special Sponsors**

- [DevOps 全流程平台](https://e.coding.net/?utm_source=weekly)

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
