Javascript Optional Chaining

最近看到一个ECMAScript新动态——Optional Chaining在6月5号进入了stage2。Stage2表明委员会已经认可这个新feature,并希望最终能加入到ECMAScript标准中去。我对Optional Chaining(以下简称OC)还是挺感兴趣的,本文就借此机会谈一谈这个新特性。

概述

OC是一个很有名的语法,C#、Swift、Kotlin、Ruby等很多知名语言都有实现。虽然语义上有些许差异,不过大致方向基本相同,语法基本都是以问号和点(?.)的形式表示。一般来说,OC主要有以下三种使用场景,静态调用、动态调用、函数调用:

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

?.前面的变量为null或是undefined时,直接返回undefined

// undefined if `a` is null/undefined, `a.b` otherwise.
a?.b
a == null ? undefined : a.b
// undefined if `a` is null/undefined, `a[x]` otherwise.
a?.[x]
a == null ? undefined : a[x]
// undefined if `a` is null/undefined, throws a TypeError if `a.b` is not a function,
//otherwise, evaluates to `a.b()`
a?.b()
a == null ? undefined : a.b()
// undefined if `a` is null/undefined,throws a TypeError if `a` is neither null/undefined,
//nor a function invokes the function `a` otherwise
a?.()
a == null ? undefined : a()

它主要解决的问题是:当访问树状结构的对象时,需要逐个判断中间节点是否有效。举个例子,我们想获取地址里的街道信息,但是并不确定地址本身是否存在,因此只能在获取街道前,事先判断一下地址合法性,JS中一般有如下三种写法:

if( address ) {
  var street = address.street;
}

var street = address ? address.street : undefined;

var street = address && address.street;

OC的写法如下(还是能短几个字符的):

var street = address?.street;

上面的例子比较简单,但是更深层次的结构,比如从国别、省份、城市、街道一路下去寻找地址信息,每一层都需要判断是否为undefined,这个代码就会很恶心了。如果使用OC语法糖,可以急速提高可读性:

let street = nation?.province?.city?.street

Babel

得益于Babel的插件@babel/plugin-proposal-optional-chaining,我很早就在开发中使用OC了。

方法很简单,先安装babel的OC插件,再在配置文件里加一行plugins即可,心动的朋友们马上可以尝试了。

yarn add -D @babel/plugin-proposal-optional-chaining
//.babelrc
{
  "plugins": ["@babel/plugin-proposal-optional-chaining"]
}

需要注意的是:一般我们都会用eslint,事先得加上新的parserOptions,不然lint会一直报错。

//.eslintrc.js
module.exports = {
  parserOptions: {
    parser: 'babel-eslint'
  },
  ...
}

Typescript

TS是JS的超级,不过由于种种原因TS也没实现这个语法糖。我曾经试过在TS里用babel插件转义OC,不过VS Code没法支持,遂放弃。
后来找了些折中的方案。

  1. ramda pahtOr

    R.pathOr('N/A', ['a', 'b'], {a: {b: 2}}); //=> 2
    R.pathOr('N/A', ['a', 'b'], {c: {b: 2}}); //=> "N/A"
    

    没用过ramda的同学可能看起来有点费劲,pathOr的参数是从右到左看的,等价于:

    const obj = {a: {b: 2}};
    const res = obj?.a?.b ?? 'N/A'
    

    说到这里我顺便提一下??这个语法糖,叫nullish coalescing operator,也是最近刚被提到stage2的新特性,它是用来代替||的。在上面的例子里,用||并且b=0的话,res = 0 || 'N/A',返回就错了;使用??就是来避免这类bug的。

  2. loadash _get

    loadash参数正好与ramda相反,从左到右,而且第二个参数可以是Array或String:

    _.get({ a: { b: 2 } }, ['a', 'b'], 'N/A');
    _.get({ a: { b: 2 } }, 'a.b', 'N/A');
    
  3. ts-optchain

    后来我又找到了一个更好玩的库,只要给第一级对象包一层oc方法,就可以一路点下去了;链路够长的话,甚至比?.语法糖更节省字符。

    import { oc } from 'ts-optchain';
    const obj: T = { /* ... */ };
    const value = oc(obj).propA.propB.propC(defaultValue);
    

    前提是给tsconfig.json加一个compiler plugins:

    // tsconfig.json
    {
        "compilerOptions": {
            "plugins": [
                { "transform": "ts-optchain/transform" },
            ]
        },
    }
    

边界情况

一般开发中,我们掌握上面概述里的OC语法其实也够用了。不过某些场景下,可能会出现一些歧义。

短路

如下,如果a不为null或undefined,x会自增。

a?.[++x]

但是a为null或undefined时,怎么办呢?应该是x不变,理由是OC本质上是一种语法糖,最终会转换为如下三元表达式,自然不会调用++x

a == null ? undefined : a[++x]

安全调用

在JS的OC里,作用域仅限于调用处,假如后续只用.不使用?.,调用安全是不保障的,就是说如果某一层出现undefined,JS会抛出异常。

a?.b.c(++x).d
a == null ? undefined : a.b.c(++x).d

也许你会觉得这个有什么好争议的。但是,某些语言(如C#、CoffeeScript)会将安全保护一路延续下去;还有上面提到的ts-optchain也是这么使用的——a?.b.c(++x).d会等价于a?.b?.c?.(++x)?.d。对某些开发人员来说,你认为的理所当然可能会导致他人极大的困惑。

Delete

OC是支持安全删除的,这点我不是很能理解,但是委员会的解释是:“为什么不支持呢?”嗯,有理有据,无可辩驳。

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

分组

(a?.b).ca?.b.c是不一样的。

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

注意到没?括号优先级是高于点,在a为undefined时,解析出了undefined.c——这个会抛异常的。所以在使用OC时,尽量不要添加括号,以免引起不必要的麻烦。

轶事

后来我又看了一下Q&A版块,还是挺欢乐的。比如:

  1. 为什么语法是?.而不是.?
  2. 为什么null?.b的结果不是null

是不是很无聊的问题?这是委员每天都在争论的话题。看看它们的回答:

  1. .?会与三元表达式冲突,比如1.?foo : bar
  2. 由于.表达式不关心.前面对象的类型,它的目的是访问.后面的属性,因此不会因为null?.b就返回null,而是统一返回undefined

还有一些边边角角的特性,比如:

  • 安全的 construction: new a?.()
  • 安全的 template literal:a?.`string`
  • 安全的赋值: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)

这些问题对开发者的理解成本较大,有些会支持,有些不会支持,有些委员会甚至都不想讨论了。这也难过OC提案这么多年才刚刚突破stage2。

小结

以前我也觉得OC这么甜的语法糖,应该尽早推出,不该整天瞎逼逼些无聊的话题。但事实上语言设计者的思考比我等深远许多。JS本身就是很好的反面教材,当年语言设计过于冲忙,直接导致了许多语法级别的bug;之后积重难返,给web开发留下了无数的巨坑。一个新语法的特性不是三言两语,或是拍拍脑袋就决定的。我就没有考虑过OC有这么多边界问题,其实归根结底还是自己碰到的具体案例太少,没有思考过特定场合的语义特性。有时候我们在埋怨某些语言多年止步不前时候,也可以思考一下其中的难点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容