精讀《Optional chaining》 1. 引言 2. 概述&精讀 3. 總結

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

如果你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

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

special Sponsors

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章