備受開發者喜好的特性 Optional chaining 在 2019.6.5 進入了 stage2,讓咱們詳細讀一下草案,瞭解一下這個特性的用法以及討論要點。前端
藉着此次精讀草案,讓咱們瞭解一下一個完整草案的標準文檔結構是怎樣的。git
一個新特性的文檔,首先要描述 原由 是什麼,也就是爲何要增長這個特性,你們不會沒有理由的就增長一個特性。其次是其餘語言是否有現成的實現版本,參考他們並進行概括總結,能夠增長思考角度的全面性。github
第三點就是 語法介紹,也就進入了新特性的正題,這裏要詳細介紹全部可能的使用狀況。第四點是 語義,也就是詮釋語法的含義。web
而後是可選的 是否有不支持的狀況,對於不支持的點是否有意而爲之,爲何?此處通常會留下討論的 ISSUE。而後是 暫不考慮的點,是因爲性價比低、使用場景少,或者實現成本高的緣由,爲何某些已經想到的點暫不考慮,這裏也會留下討論的 ISSUE。後端
後面通常還有 「正在討論的點」、「FAQ」、「草案進度」、「參考文獻」、「相關問題」、「預先討論資料」 等內容。瀏覽器
首先讓咱們回顧一下什麼是 「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.address
爲 undefined
,那 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 特性。
當 ?.
前面的變量值爲 null
或 undefined
時,?.
返回的結果爲 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]
複製代碼
第一個例子,若是 a
時 null/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.c
、c(++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?.b
、c?.(x)
的訪問與調用是安全的,而對於 b[3]
、 b[3].c
、c?.(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
複製代碼
咱們會發現,因爲括號提升了優先級,致使在 a
爲 null/undefined
時,解析出了 undefined.c
這個一定報錯的荒謬語法。所以咱們不要試圖爲 Optional chaining 進行括號分組,這樣會打破邏輯順序,使安全保護不但不生效,反而致使報錯。
中文大概能夠翻譯爲 「安全刪除」 吧,也就是 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
便可),因此就支持它吧!
下面三個特性不支持,緣由是沒什麼使用場景:
new a?.()
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
筆者總結一下,一共有這幾種使人煩惱的地方,致使你們不想支持 安全賦值 特性:
短路特性致使的理解成本:
好比 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 (a?.b in c), for (a?.b of c)
總和這幾種考慮,支持安全賦值會帶來更多靈活的用法,致使代碼複雜度陡增(想一想你的同事大量使用上面的後四種例子,你絕對想要找他決鬥,由於這種寫法和亂用 window 變量同樣,在 JS 容許的框架內寫出難以維護的邏輯,像是鑽了法律的孔子),所以 TC39 決定不支持這種用法,從源頭上杜絕被濫用。
以上不支持的功能點會在靜態編譯時被禁止,但之後也許會從新討論。
另外對於 Class 的私有變量是否支持 a?.#b
a?.#b()
還在討論中,這取決於私有成員變量草案是否能最終落地。
目前有兩個 Optional chaining 功能點暫不討論,分別是 Optional spread 與 Optional 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:
?.
而不是 .?
?緣由是與三元運算符衝突了,思考下面的用法:
1.?foo : bar
複製代碼
在 js 中,1.
等價於 1
,那麼這就是一個標準的三元運算表達式,所以 .?
語法會產生歧義,只能選擇 ?.
。
null?.b
的結果不是 null
呢?因爲 .
表達式不關心 .
前面對象的類型,由於它的目的是訪問 .
後面的屬性,所以不會由於 null?.b
就返回 null
,而是統一返回 undefined
。
最後,須要 TC39 最終審覈後,Optional chaining 才能進入 Stage3,咱們拭目以待吧!
寫一篇 JS 特性草案的完整解讀真的很累,之後也許不多有機會這麼完整的解讀草案了,但但願藉着此次解讀 Optional chaining 的機會,讓你們理解 TC39 是如何制定草案的,草案都在討論什麼,怎麼討論的,流程有哪些。
同時,還但願讓你們意識到,爲一個語言添加一個看似簡單的新特性有多麼的不容易,一個簡單的 ?.
語法就牽涉到與三元運算符、分組、解構等等已存在語法的交織與衝突,因此想要安全又穩當的添加一個新特性,參與討論的人必須對 JS 語言有完整全面的理解,同時也要對邊界狀況考慮的很周全,懂得對語法融會貫通。
最後,但願你們能夠意識到,JS 這麼重量級的語言,一個新的語法特性其實也是這麼三言兩語討論下來的,其中不乏有一些拍腦殼的地方、對於「便可也可」的狀況,稍稍結合一些具體案例就定下來其中一種的現象也是存在的,甚至對於某些規範點根本不存在一個完美的 「真理」,好比爲何語法是 ?.
而不是 a&.b
(Ruby 使用的就是 &.
),認清了這種狀況存在,就不會執着於 「語法的學習」,而轉向更底層,更有用的 「語義的學習」,並能經過閱讀 TC39 的草案瞭解其餘語言的實現差別,從而快速掌握其餘語言的語法。
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
special Sponsors
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)