為什么我喜歡JavaScript的Optional Chaining
JavaScript 的特性極大地改變了你的編碼方式。從 ES2015 開始,對我代碼影響最多的功能是解構(gòu)、箭頭函數(shù)、類和模塊系統(tǒng)。
截至 2019 年 8 月,一項新提案 optional chaining 達到了第3階段,這將是一個很好的改進。Optional Chaining 改變了從深層對象結(jié)構(gòu)訪問屬性的方式。
下面讓我們來看看 optional chaining 是如何通過在深度訪問可能缺少的屬性時刪除樣板條件和變量來簡化代碼的。
1. 問題
由于 JavaScript 的動態(tài)特性,對象可以有區(qū)別很大的嵌套對象結(jié)構(gòu)。
通常,你在以下情況下處理此類對象:
- 獲取遠程 JSON 數(shù)據(jù)
- 使用配置對象
- 具有 optional 屬性
雖然這為對象提供了支持不同結(jié)構(gòu)數(shù)據(jù)的靈活性,但是在訪問這些對象的屬性時會增加復雜性。
bigObject 在運行時可以有不同的屬性集:
- // One version of bigObject
- const bigObject = {
- // ...
- prop1: {
- //...
- prop2: {
- // ...
- value: 'Some value'
- }
- }
- };
- // Other version of bigObject
- const bigObject = {
- // ...
- prop1: {
- // Nothing here
- }
- };
因此,你必須手動檢查屬性是否存在:
- // Later
- if (bigObject &&
- bigObject.prop1 != null &&
- bigObject.prop1.prop2 != null) {
- let result = bigObject.prop1.prop2.value;
- }
這會產(chǎn)生很多樣板代碼。如果不需要寫這些代碼那就太好了。
讓我們看看 optional chaining 如何解決這個問題,并減少樣板條件。
2. 輕松的深入訪問屬性
讓我們設(shè)計一個保存電影信息的對象。該對象包含一個 title 屬性,以及可選的 director 和 actors。
movieSmall 對象只包含 title,而 movieFull 包含完整的屬性集:
- const movieSmall = {
- title: 'Heat'
- };
- const movieFull = {
- title: 'Blade Runner',
- director: { name: 'Ridley Scott' },
- actors: [{ name: 'Harrison Ford' }, { name: 'Rutger Hauer' }]
- };
讓我們寫一個獲取導演名字的函數(shù)。請記住,director 屬性可能會不存在:
- function getDirector(movie) {
- if (movie.director != null) {
- return movie.director.name;
- }
- }
- getDirector(movieSmall); // => undefined
- getDirector(movieFull); // => 'Ridley Scott'
if (movie.director) {...} 條件用于驗證 director 屬性是否已定義。如果沒有這個預防措施,在訪問movieSmall 對象 director 的時候,JavaScript 會拋出錯誤 TypeError: Cannot read property 'name' of undefined。
這是使用新的 optional chaining 功能的正確位置,并刪除 movie.director 的存在驗證。新版本的getDirector()看起來要短得多:
- function getDirector(movie) {
- return movie.director?.name;
- }
- getDirector(movieSmall); // => undefined
- getDirector(movieFull); // => 'Ridley Scott'
在表達式 movie.director?.name 中你可以找到 ?.: optional chaining 運算符。
在 movieSmall 的情況下,如果屬性 director 丟失了。那么 movie.director?.name 的計算結(jié)果為 undefined。 optional chaining 運算符可防止拋出 TypeError:Cannot read property 'name' of undefined。
相反,在 movieFull 的情況下,屬性 director 可用。 movie.director?.name 的值為 'Ridley Scott'.。
簡單來說,代碼片段:
- let name = movie.director?.name;
相當于:
- let name;
- if (movie.director != null) {
- name = movie.director.name;
- }
?. 通過減少 2 行代碼簡化了 getDirector() 函數(shù)。這就是我喜歡 optional chaining 的原因。
2.1 數(shù)組項
但是 optional chaining 功能可以做更多的事情。你可以在同一表達式中使用多個optional chaining 運算符。甚至可以使用它來安全地訪問數(shù)組項目!
接下來的任務(wù)是編寫一個返回電影主角名字的函數(shù)。
在 movie 對象中,actors 數(shù)組可以為空甚至丟失,因此你必須添加其他條件:
- function getLeadingActor(movie) {
- if (movie.actors && movie.actors.length > 0) {
- return movie.actors[0].name;
- }
- }
- getLeadingActor(movieSmall); // => undefined
- getLeadingActor(movieFull); // => 'Harrison Ford'
if (movie.actors && movies.actors.length > 0) {...} 條件需要確保 movie 中包含 actors 屬性,并且此屬性至少有一個 actor。
通過使用 optional chaining,此任務(wù)很容易解決:
- function getLeadingActor(movie) {
- return movie.actors?.[0]?.name;
- }
- getLeadingActor(movieSmall); // => undefined
- getLeadingActor(movieFull); // => 'Harrison Ford'
actors?. 確保 actors 屬性存在。 [0]?. 確保第一個 actor 存在于列表中。很好!
3. nullish 合并
名為 nullish coalescing operator 的新提案建議用 ?? 處理 undefined或null,將它們默認為特定的值。
如果 variable 是undefined或null,則表達式 variable ?? defaultValue 的結(jié)果為defaultValue, 否則表達式的值為variable 的值。
- const noValue = undefined;
- const value = 'Hello';
- noValue ?? 'Nothing'; // => 'Nothing'
- value ?? 'Nothing'; // => 'Hello'
當評估為 undefined 時,Nullish 合并可以通過默認值來改進 optional chaining。
例如,當 movie 對象中沒有 actor時,讓我們改變 getLeading() 函數(shù)返回 "Unknown actor":
- function getLeadingActor(movie) {
- return movie.actors?.[0]?.name ?? 'Unknown actor';
- }
- getLeadingActor(movieSmall); // => 'Unknown actor'
- getLeadingActor(movieFull); // => 'Harrison Ford'
4. optional chaining 的 3 種形式
可以用以下 3 種形式使用 optional chaining 。
第一種形式 object?.property 用于訪問靜態(tài)屬性:
- const object = null;
- object?.property; // => undefined
第二種形式 object?.[expression] 用于訪問動態(tài)屬性或數(shù)組項:
- const object = null;
- const name = 'property';
- object?.[name]; // => undefined
- const array = null;
- array?.[0]; // => undefined
最后,第三種形式 object?.([arg1,[arg2,...]]) 執(zhí)行一個對象方法:
- const object = null;
- object?.method('Some value'); // => undefined
如果需要,可以通過組合這些表單來創(chuàng)建長的可選鏈:
- const value = object.maybeUndefinedProp?.maybeNull()?.[propName];
5. 短路:停止于 null/undefined
有關(guān) optional chaining 運算符的有趣之處在于,只要在其左側(cè) leftHandSide?.rightHandSide 中遇到無效值,右側(cè)訪問器的評估就會停止。這稱為短路。
我們來看一個例子:
- const nothing = null;
- let index = 0;
- nothing?.[index++]; // => undefined
- index; // => 0
nothing 保持一個 nullish 值,因此 optional chaining 評估為 undefined ,并跳過右側(cè)訪問器的評估。因為 index 編號不會增加。
6. 何時使用 optional chaining
一定要克制使用 optional chaining 操作符訪問任何類型屬性的沖動:這將會導致誤導使用。下一節(jié)將介紹何時正確使用它。
6.1 訪問可能無效的屬性
?. 必須只在可能無效的屬性附近使用:maybeNullish?.prop。在其他情況下,使用舊的屬性訪問器:.property 或 [propExpression]。
回想一下 movie 對象。查看表達式 movie.director?.name,因 為director 可以是 undefined,在director屬性附近使用 optional chaining 運算符是正確的。
相反,使用 ?. 來訪問電影標題是沒有意義的:movie?.title。movie 對象不會是無效的。
- // Good
- function logMovie(movie) {
- console.log(movie.director?.name);
- console.log(movie.title);
- }
- // Bad
- function logMovie(movie) {
- // director needs optional chaining
- console.log(movie.director.name);
- // movie doesn't need optional chaining
- console.log(movie?.title);
- }
6.2 通常有更好的選擇
以下函數(shù) hasPadding() 接受帶有可選 padding 屬性的樣式對象。 padding 具有可選屬性left、top、right、bottom。
下面嘗試使用 optional chaining 運算符:
- function hasPadding({ padding }) {
- const top = padding?.top ?? 0;
- const right = padding?.right ?? 0;
- const bottom = padding?.bottom ?? 0;
- const left = padding?.left ?? 0;
- return left + top + right + bottom !== 0;
- }
- hasPadding({ color: 'black' }); // => false
- hasPadding({ padding: { left: 0 } }); // => false
- hasPadding({ padding: { right: 10 }}); // => true
雖然函數(shù)正確地確定元素是否具有填充,但是對于每個屬性都使用 optional chaining 是非常困難的。
更好的方法是使用對象擴展運算符將填充對象默認為零值:
- function hasPadding({ padding }) {
- const p = {
- top: 0,
- right: 0,
- bottom: 0,
- left: 0,
- ...padding
- };
- return p.top + p.left + p.right + p.bottom !== 0;
- }
- hasPadding({ color: 'black' }); // => false
- hasPadding({ padding: { left: 0 } }); // => false
- hasPadding({ padding: { right: 10 }}); // => true
在我看來,這個版本的 hasPadding() 更容易閱讀。
7. 為什么我喜歡它?
我喜歡 optional chaining 運算符,因為它允許從嵌套對象輕松訪問屬性。它可以減少通過編寫樣板文件來驗證來自訪問器鏈的每個屬性訪問器上無效值的工作。
當 optional chaining 與無效合并運算符組合時,你可以獲得更好的結(jié)果,能夠更輕松地處理默認值。