最近看到一個(gè)ECMAScript新動(dòng)態(tài)——Optional Chaining在6月5號(hào)進(jìn)入了stage2。Stage2表明委員會(huì)已經(jīng)認(rèn)可這個(gè)新feature土童,并希望最終能加入到ECMAScript標(biāo)準(zhǔn)中去。我對(duì)Optional Chaining(以下簡(jiǎn)稱OC)還是挺感興趣的几莽,本文就借此機(jī)會(huì)談一談這個(gè)新特性秉溉。
概述
OC是一個(gè)很有名的語(yǔ)法,C#璃俗、Swift、Kotlin悉默、Ruby等很多知名語(yǔ)言都有實(shí)現(xiàn)城豁。雖然語(yǔ)義上有些許差異,不過大致方向基本相同抄课,語(yǔ)法基本都是以問號(hào)和點(diǎn)(?.
)的形式表示唱星。一般來說,OC主要有以下三種使用場(chǎng)景跟磨,靜態(tài)調(diào)用间聊、動(dòng)態(tài)調(diào)用、函數(shù)調(diào)用:
obj?.prop // optional static property access
obj?.[expr] // optional dynamic property access
func?.(...args) // optional function or method call
當(dāng)?.
前面的變量為null
或是undefined
時(shí)抵拘,直接返回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()
它主要解決的問題是:當(dāng)訪問樹狀結(jié)構(gòu)的對(duì)象時(shí),需要逐個(gè)判斷中間節(jié)點(diǎn)是否有效。舉個(gè)例子尚蝌,我們想獲取地址里的街道信息迎变,但是并不確定地址本身是否存在,因此只能在獲取街道前飘言,事先判斷一下地址合法性氏豌,JS中一般有如下三種寫法:
if( address ) {
var street = address.street;
}
var street = address ? address.street : undefined;
var street = address && address.street;
OC的寫法如下(還是能短幾個(gè)字符的):
var street = address?.street;
上面的例子比較簡(jiǎn)單,但是更深層次的結(jié)構(gòu)热凹,比如從國(guó)別泵喘、省份、城市般妙、街道一路下去尋找地址信息纪铺,每一層都需要判斷是否為undefined,這個(gè)代碼就會(huì)很惡心了碟渺。如果使用OC語(yǔ)法糖鲜锚,可以急速提高可讀性:
let street = nation?.province?.city?.street
Babel
得益于Babel的插件@babel/plugin-proposal-optional-chaining,我很早就在開發(fā)中使用OC了苫拍。
方法很簡(jiǎn)單芜繁,先安裝babel的OC插件,再在配置文件里加一行plugins即可绒极,心動(dòng)的朋友們馬上可以嘗試了骏令。
yarn add -D @babel/plugin-proposal-optional-chaining
//.babelrc
{
"plugins": ["@babel/plugin-proposal-optional-chaining"]
}
需要注意的是:一般我們都會(huì)用eslint,事先得加上新的parserOptions垄提,不然lint會(huì)一直報(bào)錯(cuò)榔袋。
//.eslintrc.js
module.exports = {
parserOptions: {
parser: 'babel-eslint'
},
...
}
Typescript
TS是JS的超級(jí),不過由于種種原因TS也沒實(shí)現(xiàn)這個(gè)語(yǔ)法糖铡俐。我曾經(jīng)試過在TS里用babel插件轉(zhuǎn)義OC凰兑,不過VS Code沒法支持,遂放棄审丘。
后來找了些折中的方案吏够。
-
R.pathOr('N/A', ['a', 'b'], {a: {b: 2}}); //=> 2 R.pathOr('N/A', ['a', 'b'], {c: {b: 2}}); //=> "N/A"
沒用過ramda的同學(xué)可能看起來有點(diǎn)費(fèi)勁,pathOr的參數(shù)是從右到左看的滩报,等價(jià)于:
const obj = {a: {b: 2}}; const res = obj?.a?.b ?? 'N/A'
說到這里我順便提一下
??
這個(gè)語(yǔ)法糖锅知,叫nullish coalescing operator,也是最近剛被提到stage2的新特性露泊,它是用來代替||
的喉镰。在上面的例子里旅择,用||
并且b=0
的話惭笑,res = 0 || 'N/A'
,返回就錯(cuò)了;使用??
就是來避免這類bug的沉噩。 -
loadash參數(shù)正好與ramda相反捺宗,從左到右,而且第二個(gè)參數(shù)可以是Array或String:
_.get({ a: { b: 2 } }, ['a', 'b'], 'N/A'); _.get({ a: { b: 2 } }, 'a.b', 'N/A');
-
后來我又找到了一個(gè)更好玩的庫(kù)川蒙,只要給第一級(jí)對(duì)象包一層oc方法蚜厉,就可以一路點(diǎn)下去了;鏈路夠長(zhǎng)的話畜眨,甚至比
?.
語(yǔ)法糖更節(jié)省字符昼牛。import { oc } from 'ts-optchain'; const obj: T = { /* ... */ }; const value = oc(obj).propA.propB.propC(defaultValue);
前提是給tsconfig.json加一個(gè)compiler plugins:
// tsconfig.json { "compilerOptions": { "plugins": [ { "transform": "ts-optchain/transform" }, ] }, }
邊界情況
一般開發(fā)中,我們掌握上面概述里的OC語(yǔ)法其實(shí)也夠用了康聂。不過某些場(chǎng)景下贰健,可能會(huì)出現(xiàn)一些歧義。
短路
如下恬汁,如果a不為null或undefined伶椿,x會(huì)自增。
a?.[++x]
但是a為null或undefined時(shí)氓侧,怎么辦呢脊另?應(yīng)該是x
不變,理由是OC本質(zhì)上是一種語(yǔ)法糖约巷,最終會(huì)轉(zhuǎn)換為如下三元表達(dá)式偎痛,自然不會(huì)調(diào)用++x
。
a == null ? undefined : a[++x]
安全調(diào)用
在JS的OC里独郎,作用域僅限于調(diào)用處看彼,假如后續(xù)只用.
不使用?.
,調(diào)用安全是不保障的囚聚,就是說如果某一層出現(xiàn)undefined靖榕,JS會(huì)拋出異常。
a?.b.c(++x).d
a == null ? undefined : a.b.c(++x).d
也許你會(huì)覺得這個(gè)有什么好爭(zhēng)議的顽铸。但是茁计,某些語(yǔ)言(如C#、CoffeeScript)會(huì)將安全保護(hù)一路延續(xù)下去谓松;還有上面提到的ts-optchain
也是這么使用的——a?.b.c(++x).d
會(huì)等價(jià)于a?.b?.c?.(++x)?.d
星压。對(duì)某些開發(fā)人員來說,你認(rèn)為的理所當(dāng)然可能會(huì)導(dǎo)致他人極大的困惑鬼譬。
Delete
OC是支持安全刪除的娜膘,這點(diǎn)我不是很能理解,但是委員會(huì)的解釋是:“為什么不支持呢优质?”嗯竣贪,有理有據(jù)军洼,無(wú)可辯駁。
delete a?.b
a == null ? true : delete a.b
分組
(a?.b).c
和a?.b.c
是不一樣的演怎。
(a?.b).c
(a == null ? undefined : a.b).c
a?.b.c
a == null ? undefined : a.b.c
注意到?jīng)]匕争?括號(hào)優(yōu)先級(jí)是高于點(diǎn),在a為undefined
時(shí)爷耀,解析出了undefined.c
——這個(gè)會(huì)拋異常的甘桑。所以在使用OC時(shí),盡量不要添加括號(hào)歹叮,以免引起不必要的麻煩跑杭。
軼事
后來我又看了一下Q&A版塊,還是挺歡樂的咆耿。比如:
- 為什么語(yǔ)法是
?.
而不是.?
- 為什么
null?.b
的結(jié)果不是null
是不是很無(wú)聊的問題艘蹋?這是委員每天都在爭(zhēng)論的話題∑被遥看看它們的回答:
-
.?
會(huì)與三元表達(dá)式?jīng)_突女阀,比如1.?foo : bar
- 由于
.
表達(dá)式不關(guān)心.
前面對(duì)象的類型,它的目的是訪問.
后面的屬性屑迂,因此不會(huì)因?yàn)?code>null?.b就返回null
浸策,而是統(tǒng)一返回undefined
還有一些邊邊角角的特性,比如:
- 安全的 construction: new a?.()
- 安全的 template literal:a?.`string`
- 安全的賦值:a?.b = c
- 自增惹盼,自減:a?.b++, --a?.b
- 解構(gòu)賦值: { x: a?.b } = c, [ a?.b ] = c
- for 循環(huán)中的臨時(shí)賦值:for (a?.b in c), for (a?.b of c)
這些問題對(duì)開發(fā)者的理解成本較大庸汗,有些會(huì)支持,有些不會(huì)支持手报,有些委員會(huì)甚至都不想討論了蚯舱。這也難過OC提案這么多年才剛剛突破stage2。
小結(jié)
以前我也覺得OC這么甜的語(yǔ)法糖掩蛤,應(yīng)該盡早推出枉昏,不該整天瞎逼逼些無(wú)聊的話題。但事實(shí)上語(yǔ)言設(shè)計(jì)者的思考比我等深遠(yuǎn)許多揍鸟。JS本身就是很好的反面教材兄裂,當(dāng)年語(yǔ)言設(shè)計(jì)過于沖忙,直接導(dǎo)致了許多語(yǔ)法級(jí)別的bug阳藻;之后積重難返晰奖,給web開發(fā)留下了無(wú)數(shù)的巨坑。一個(gè)新語(yǔ)法的特性不是三言兩語(yǔ)腥泥,或是拍拍腦袋就決定的匾南。我就沒有考慮過OC有這么多邊界問題,其實(shí)歸根結(jié)底還是自己碰到的具體案例太少蛔外,沒有思考過特定場(chǎng)合的語(yǔ)義特性蛆楞。有時(shí)候我們?cè)诼裨鼓承┱Z(yǔ)言多年止步不前時(shí)候溯乒,也可以思考一下其中的難點(diǎn)。