前端開發 🦏 來談 JavaScript 的 Optional Chaining 和 Nullish Coalescing (一)
筆者為 Web / Backend / iOS / Android 軟體開發者,
《程式猿吃香蕉🍌》客座技術專家
自從 ECMAScript 6 以降,JavaScript 生態系的開發越來越倚賴使用更新的語法來撰寫程式,再以 Babel 等工具將原始碼轉譯成相容現代瀏覽器語法的 JavaScript 碼來發布。在眾多新加入的語法中,本文要來探討的是由 Ecma Technical Comitee 39 (Ecma TC39) 提出的 Optional Chaining、以及一定會跟著提到的 Nullish Coalescing 語法。
這篇預計會是個前後拆分的系列文,上篇會先簡單介紹兩種語法的使用方式和歷史緣由,再整理目前主流的開發環境對它們的支援程度。
本篇內容:
✔ 在還沒有 Optional Chaining 之前...
✔ 使用 Optional Chaining 簡化安全檢查
✔ 進一步補齊不足:Nullish Coalescing
✔ 可以開始使用這些語法撰寫程式了嗎?
✔ 小結
▍在還沒有 Optional Chaining 之前…
在 JavaScript 這個弱型別 (weak typing) 的世界中,幾乎任何變數都可以被視為一種物件來看待,並利用 dot ( .
)、bracket notation ( [key]
) 等方式取得物件的屬性 (property)。如同大部分的程式語言,存取複雜結構物件深處的屬性也很直覺,例如
const articles = [
{
id: 101,
title: 'First Article',
author: { id: 101, name: 'John' },
},
{
id: 102,
title: 'Second Article',
},
{
id: 103,
title: 'Article 3',
author: { id: 102, name: 'Austin' },
},
]
articles.forEach(article => {
const authorName = article.author.name;
});
在這個例子中, forEach
迴圈內會先取得 article
的 author
屬性、再馬上取得 author
屬性這個物件底下的 name
屬性,並指派給 authorName
。雖說 articles[0]
和 articles[2]
都不會有問題,但只要試著存取 articles[1]
的 author.name
的話,就會因為 articles[1].author
相當於 undefined
的緣故,造成程式中斷並拋出 (throw) 下列錯誤:
這是因為 JavaScript 的語法規範中,不允許針對 null
或 undefined
等所謂的「Nullish value」進行屬性存取,會被視為執行期間的錯誤 (runtime error)。雖說如此,在結構複雜、構造不一致的資料中,這是相當常碰到的狀況,因此傳統來說 JavaScript 開發者們普遍會做安全檢查,以避免跑出此錯誤:
const authorName = article && article.author
? article.author.name
: undefined;
這裡利用了三元運算子 (ternary operator)、以及 &&
的短路性質 (short-circuit evaluation),先判斷 article
和 article.author
都是 truthy value、不會造成上述的 TypeError
後,再將欲取得的 article.author.name
指派給 authorName
,若否則指派 undefined
。
然而,一旦需要這樣處理的資料類型變多、程式碼內充滿著這種防止錯誤的安全檢查的時候,整體的可讀性會下降許多。某些情況下可以直接使用 try catch
防止發生錯誤,但同樣會有可讀性下降以及誤吃掉不該被忽略的錯誤的可能性。
▍使用 Optional Chaining 簡化安全檢查
因為類似的需求越來越普遍,JavaScript 開發者們很希望能有一個更簡潔的寫法、一個語法糖 (syntax sugar) 來應付這些繁雜的安全檢查;於是 Optional Chaining 語法被加入到 TC39 proposal 的一員,使用「以回傳 undefined
代替拋出錯誤」的概念來精簡程式碼:
const authorName = article?.author?.name;
從普通的 dot notation 取值改成 ?.
後,當運算子左邊的值並非 null
或 undefined
時,才會繼續往右手邊取值下去,否則便直接回傳 undefined
。因此在上述的 articles
範例中, articles[1]
的 authorName
就會變成 undefined
、而非造成執行錯誤,其執行結果和上述的安全檢查的寫法幾乎互為等價(考量到實際上 JavaScript 做 falsy 判斷的機制,此處加上「幾乎」以排除特定的特殊例子)。
除了 dot notation 外,Optional Chaining 語法規範內也有定義 bracket notation 的使用方式,例如
const authorName = articles?.[index]?.author?.['name'];
用於函式呼叫 (function calling)
另外一個常見的錯誤情境在呼叫函式時也會發生:當我們要將一個物件的屬性視為函式呼叫、但該屬性不存在時便會發生,例如
const x = { amount: 100 };
const y = x.func(); // Uncaught TypeError: x.func is not a function
Optional Chaining 語法規範同樣提供了 “optional function calling” 的機制:當該屬性不為 null
或 undefined
時,程式便會執行該函式,否則整個呼叫的宣告會直接回傳 undefined
:
const x = { amount: 100 };
const y = x.func?.(); // y becomes undefined
附帶一提,後兩者之所以不直接採用 ?[key]
、 ?()
這樣的語法,是因為這樣會和三元運算子的 ?
混淆,因此設計成了在三元運算子當中不會出現的 ?.
組合來做為前綴。
▍進一步補齊不足:Nullish Coalescing
在實際開發上,只是簡化屬性存取的安全檢查還不夠,往往還需要在沒有取得值的同時賦予一個預設值 (default value)。傳統上在 JavaScript 中,當我們要替一個宣告設定預設值的時候,常常會用 ||
運算子進行。
假設我們現在要撰寫一個可以動態設定介面樣式的行為,要從 API 取得一個高度的設定值用以設定元素的高度,而如果 API 沒有提供此欄位時則預設高度為 300
。大略的程式碼如下:
const response = await fetch(GET_CONFIG_API);
const itemHeight = response?.data?.height || 300;
document.getElementById('item').style.height = `${itemHeight}px`;
這樣一來,當 API 沒有提供 height
欄位、或是萬一 response
或 data
為空值時, itemHeight
就會自動被指派成 ||
後面的 300
。
但是若 height
回傳了 0
的話,跑出來的結果就會和原本想的不一樣:在 JavaScript 語法中, 0
是一個 falsy value,亦即 ||
的判斷是會跳過前面的 response.data.height
,導致itemHeight
被指派成 300
。為了避免這種情況發生,就只能放棄使用 ||
、採取額外的安全檢查措施:
let itemHeight = response?.data?.height;
if (itemHeight === null || itemHeight === undefined) {
itemHeight = 300;
}
// Or...
if (!(itemHeight >= 0)) {
itemHeight = 300;
}
在這個情境下,Nullish Coalescing 便可以拿來更精確簡潔的解決這種「預設取值」的問題。
具體來說,Nullish Coalescing 語法定義了一個運算子 ??
:當運算子的左手邊 (left-hand side, LHS) 值為 null
或 undefined
的話,便會回傳右手邊 (right-hand side, RHS) 值。因此,上述的安全檢查可以改寫成:
itemHeight = itemHeight ?? 300;
兩個語法的組合
當然更進一步的,Optional Chaining 和 Nullish Coalescing 可以寫在一起,變成最簡潔的形式:
const itemHeight = response?.data?.height ?? 300;
這樣一來,首先若 response
或 data
為空值的話,程式不會中斷拋出錯誤,左手邊值會變成 undefined
。再來,如果左手邊的值結果是空值的話,整個指派式會回傳 ?? 右手邊的 300
,讓 itemHeight
一定會被指派到一個 null
/ undefined
以外的數值;而且,若 response.data.height
為 0
, itemHeight
就會被指派成 0
而非 300
,避免 ||
的 falsy check 帶來的困擾。
實務上,這種多重 Optional Chaining 搭配 Nullish Coalescing 的寫法滿常見的,可以確實的讓程式碼減少繁瑣的安全判斷、寫出可讀性更高的程式。
▍可以開始使用這些語法撰寫程式了嗎?
在筆者撰寫此篇文章的 2020 年十一月底時間點,Optional Chaining 和 Nullish Coalescing 語法在 TC39 proposal 的階段 (current stage) 皆已進入 ECMAScript 2017(或 ES8)的 Stage 4,幾乎可說已經是官方語法了。已經有許多工具和開發環境支援:
- Node.js 於 14.0 開始預設支援 Optional Chaining 與 Nullish Coalescing 語法。
- TypeScript 於 3.7 開始同時支援此二語法,使用方式和語法規格完全比照 TC39 proposal 的規格,時間點可算是這個列表當中最早的一個。
- Chrome、Firebox、Safari 等瀏覽器也已全面支援,受益於各自的 JavaScript engine:V8、SpiderMonkey 和 WebKit (JSC) 均已於 2019 年底開始陸續加入語法實作。現在只要開啟瀏覽器的主控台,應該就能直接開始實際測試撰寫 Optional Chaining 和 Nullish Coalescing 語法。
- Babel 有針對兩個語法的 experimental plugin:@babel/plugin-proposal-optional-chaining 和 @babel/plugin-proposal-nullish-coalescing-operator。Babel 自家的 env preset 也有加入這兩個 plugin,因此大部分情況下只要直接使用 env preset 即可。
- Create-React-App 從 3.4.1 開始支援(藉由他們自己的 react-app preset)
- 在 Acorn v7 加入這兩個語法的支援之後,Webpack v5 也開始支援此二語法。
- Vue CLI 或 Nuxt.js 專案同樣使用到 Babel env preset,因此也有直接支援。
- ESLint 於 v7.2.0 加入 Nullish Coalescing 語法支援、並稍後於 v7.5.0 加入 Optional Chaining 支援;不過若是專案使用 Babel 做語言轉換,通常直接使用 @babel/eslint-parser 即可。
- Flow 於 0.71.0 就能開始使用 Optional Chaining,不過從 0.133.0 開始便正式將這兩個語法設定成預設值,對應他們進入 Stage 4 的時間點。
總之,在這個連 ECMAScript 2020 都已經進入熱烈討論的時間點,Optional Chaining 和 Nullish Coalescing 語法實在已經不算是什麼最新玩意、也已經被大部分開發環境所接受,若還沒嘗試使用這些語法撰寫程式的話推薦可以開始加入專案使用看看。
▍小結
以上是這個系列文的 Part I,筆者大致介紹了 Optional Chaining 和 Nullish Coalescing 兩個 ECMAScript 2017 的新語法、並整理現階段開發時有哪些環境和工具能夠使用這兩個語法。下一篇 Part II 會來跟其他程式語言的類似語法進行比較,以及更深入探討 Optional Chaining 語法的各種細節,敬請期待!
※ 12/10 更新 Part II:
參考資料
- Optional Chaining Proposal: https://github.com/tc39/proposal-optional-chaining
- Nullish Coalescing Proposal: https://github.com/tc39/proposal-nullish-coalescing
- Node.js v14.0.0 Changelog: https://nodejs.org/en/blog/release/v14.0.0/
- Announcing TypeScript 3.7: https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/
- Create-React-App v3.4.1 Changelog: https://github.com/facebook/create-react-app/releases/tag/v3.4.1
- JS Engine Implementation Status Tracking: https://github.com/tc39/proposal-optional-chaining/issues/115
- Babel Optional Chaining Plugin: https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining
- Babel Nullish Coalescing Operator Plugin: https://babeljs.io/docs/en/babel-plugin-proposal-nullish-coalescing-operator
- Acorn v7.0.0: https://github.com/acornjs/acorn/releases/tag/7.0.0
- ESLint release notes v7.2.0: https://eslint.org/blog/2020/06/eslint-v7.2.0-released
- ESLint release notes v7.5.0: https://eslint.org/blog/2020/07/eslint-v7.5.0-released
- Flow 0.133.0 Changelog: https://github.com/facebook/flow/releases/tag/v0.133.0