1. 引言

备受开发者喜爱的特性 Optional chaining 在 2019.6.5 进入了 stage2,让我们详细读一下草案,了解一下这个特性的用法以及讨论要点。

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

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

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

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

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

2. 概述&精读

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

起因介绍

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

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

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

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

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

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

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

正如上面的例子:如果 user.addressundefined,那 street 拿到的就是 undefined,而不是报错。

配合另一个在 stage2 的新特性 Nullish Coalescing 做默认值处理非常方便:

// 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;

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

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 也可以用在方法上:

iterator.return?.()

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

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 的读者可以 点击阅读原文

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

语法

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

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 特性。

语义

.? 前面的变量值为 nullundefined 时,.? 返回的结果为 undefined

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 后,某些看似一定会执行的语句在特定情况下会短路(终止执行),比如:

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

第一个例子,如果 anull/undefined,就不会执行 ++x

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

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

长“短路”

Optional chaining 在 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.cc(++x).d 继续生效。而对于 C+ 与 CoffeeScript,这个语法是对后续所有 get 生效的(这里再次提醒,不要用 CoffeeScript 了,因为对于相同语法,语义都发生了变化,对你与你的同事都是巨大的理解负担,或者说没有人愿意注意,为什么代码在 CoffeeScript 里不报错,而转移到 JS 就报错了,是因为 Optional chaining 语义不一致造成的。)。

正因为 Optional chaining 在 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.?bc?.(x) 的访问与调用是安全的,而对于 b[3]b[3].cc?.(x).d 的调用是不安全的。

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

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

a?.b()

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

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

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

边界情况 - 分组

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

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

与不带括号的进行对比:

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

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

Optional delete

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

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 来说,比如下面的语法:

a?.b
`c`

会被解析为

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

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

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

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

div`
width: 300px;
`

而如果解析为:

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

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

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

  • 安全的赋值:a?.b = c

讨论 ISSUE

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

短路特性导致的理解成本:

比如 a?.b = c(),如果 anull/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 spreadOptional destructuring

对于 Optional spread,建议是:

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

但由于可以结合 Nullish Coalescing 达到同样的效果:

foo(...args ?? [])

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

对于 Optional destructuring,建议是:

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

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

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

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

或者

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

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

余下的 Q&A

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

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

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

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

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

special Sponsors

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

精读《Optional chaining》的更多相关文章

  1. 精读《V8 引擎 Lazy Parsing》

    1. 引言 本周精读的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎为了优化性能,做了怎样的尝试吧! 这篇文章介绍的优化技术叫 preparser,是通过跳过不必要函数编译的方式优化性 ...

  2. 深入浏览器工作原理和JS引擎(V8引擎为例)

    浏览器工作原理和JS引擎 1.浏览器工作原理 在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?以及JavaScript代码在浏览器中是如何被执行的? 大概流程可观察以下图: 首先,用户在浏览器搜 ...

  3. [翻译] V8引擎的解析

    原文:Parsing in V8 explained 本文档介绍了 V8 引擎是如何解析 JavaScript 源代码的,以及我们将改进它的计划. 动机 我们有个解析器和一个更快的预解析器(~2x), ...

  4. 一文搞懂V8引擎的垃圾回收

    引言 作为目前最流行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在浏览器和Nodejs这两大宿主环境中,也是因为背后有强大的V ...

  5. Chrome V8引擎系列随笔 (1):Math.Random()函数概览

    先让大家来看一幅图,这幅图是V8引擎4.7版本和4.9版本Math.Random()函数的值的分布图,我可以这么理解 .从下图中,也许你会认为这是个二维码?其实这幅图告诉我们一个道理,第二张图的点的分 ...

  6. (译)V8引擎介绍

    V8是什么? V8是谷歌在德国研发中心开发的一个JavaScript引擎.开源并且用C++实现.可以用于运行于客户端和服务端的Javascript程序. V8设计的初衷是为了提高浏览器上JavaScr ...

  7. 浅谈Chrome V8引擎中的垃圾回收机制

    垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...

  8. V8引擎嵌入指南

    如果已读过V8编程入门那你已经熟悉了如句柄(handle).作用域(scope)和上下文(context)之类的关键概念,以及如何将V8引擎作为一个独立的虚拟机来使用.本文将进一步讨论这些概念,并介绍 ...

  9. 浅谈V8引擎中的垃圾回收机制

    最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...

  10. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

随机推荐

  1. css不同情况下的各种居中方法

    div水平居中 1.行内元素 .parent{ text-align: center } 2.块级元素 .son{ margin: 0 auto ; } 3.flex布局 .parent{ displ ...

  2. samba服务和client挂载

    服务端 1.安装samba服务 yum -y install samba 2.创建系统用户 因为Samba 服务程序的数据库要求账户必须在当前系统中已经存在,否则日后创建文件时将导致文件的权限属性混乱 ...

  3. Nginx 502 Bad Gateway 的错误的解决方案

    我用的是nginx反向代理Apache,直接用Apache不会有任何问题,加上nginx就会有部分ajax请求502的错误,下面是我收集到的解决方案. 一.fastcgi缓冲区设置过小 出现错误,首先 ...

  4. JDK的动态代理深入解析(Proxy,InvocationHandler)(转)

    JDK的动态代理深入解析(Proxy,InvocationHandler)(转) 一.什么是动态代理 动态代理可以提供对另一个对象的访问,同时隐藏实际对象的具体事实.代理一般会实现它所表示的实际对象的 ...

  5. 第二篇:请求库之requests,selenium

    requests模块 一.介绍 #介绍:使用requests可以模拟浏览器的请求,比起之前用到的urllib,requests模块的api更加便捷(本质就是封装了urllib3) #注意:reques ...

  6. electron 编译报错

    放在中文目录下的项目,会编译的时候报错 python 安装目录不要有空格,默认目录就好

  7. 大数乘法(A * B Problem Plus)问题

    大数乘法问题一般可以通过将大数转换为数组来解决. 解题思路 第1步 第2步 第3步 第4步 样例输入1 56 744 样例输出1 800 样例输入2 -10 678 样例输出2 -6780 样例输入3 ...

  8. Burpsuite查看和修改请求

    打开上传测试网页(此处是自己搭建的OWASP平台),这个网页只能上传图片格式的文件 上传一张图片: 查看上传图片: 创建一个test.text文件: 配置浏览器代理,IP:127.0.0.1,端口:8 ...

  9. time时间库使用示例

    time时间库主要有以下几个方法 1. 生成struct_time ,然后就可以很方便的获取到年月日,时分秒等信息 time.localtime() 2. 生成时间戳 time.time() 3. 将 ...

  10. C# 向上取整数

    PageCount = personInfodb.get_count(selected_id); //数据总数 PageCount = (int)Math.Ceiling(PageCount / (P ...