為什么我喜歡 JavaScript 可選鏈
很多 JavaScript 的特性極大地改變了你的編碼方式。從 ES2015 及更高版本開(kāi)始,對(duì)我的代碼影響較大的功能是解構(gòu)、箭頭函數(shù)、類(lèi)和模塊系統(tǒng)。
截至2019年8月,一項(xiàng)新提案可選鏈(optional chaining)進(jìn)入了第3階段,將是一個(gè)很好的改進(jìn)??蛇x的鏈接更改了從深層對(duì)象結(jié)構(gòu)訪問(wèn)屬性的方式。
讓我們看看可選鏈?zhǔn)侨绾瓮ㄟ^(guò)在深度訪問(wèn)可能缺少的屬性時(shí)刪除樣板條件和變量來(lái)簡(jiǎn)化代碼的。
1. 問(wèn)題
由于 JavaScript 的動(dòng)態(tài)特性,一個(gè)對(duì)象可以具有非常不同的對(duì)象嵌套結(jié)構(gòu)。
通常,你可以在以下情況下處理此類(lèi)對(duì)象:
- 獲取遠(yuǎn)程JSON數(shù)據(jù)
- 使用配置對(duì)象
- 具有可選屬性
盡管這為對(duì)象提供了支持不同數(shù)據(jù)的靈活性,但是在訪問(wèn)此類(lèi)對(duì)象的屬性時(shí),隨之而來(lái)的是增加了復(fù)雜性。
bigObject 在運(yùn)行時(shí)可以有不同的屬性集:
- // One version of bigObject
- const bigObject = {
- // ...
- prop1: {
- //...
- prop2: {
- // ...
- value: 'Some value'
- }
- }
- };
- // Other version of bigObject
- const bigObject = {
- // ...
- prop1: {
- // Nothing here
- }
- };
因此你必須手動(dòng)檢查屬性是否存在:
- // Later
- if (bigObject &&
- bigObject.prop1 != null &&
- bigObject.prop1.prop2 != null) {
- let result = bigObject.prop1.prop2.value;
- }
最好不要這樣寫(xiě),因?yàn)榘颂嗟臉影宕a。。
讓我們看看可選鏈?zhǔn)侨绾谓鉀Q此問(wèn)題,從而減少樣板條件的。
2. 輕松深入訪問(wèn)屬性
讓我們?cè)O(shè)計(jì)一個(gè)保存電影信息的對(duì)象。該對(duì)象包含 title 必填屬性,以及可選的 director 和 actor。
movieSmall 對(duì)象僅包含 title,而 movieFull 則包含完整的屬性集:
- const movieSmall = {
- title: 'Heat'
- };
- const movieFull = {
- title: 'Blade Runner',
- director: { name: 'Ridley Scott' },
- actors: [{ name: 'Harrison Ford' }, { name: 'Rutger Hauer' }]
- };
讓我們寫(xiě)一個(gè)獲取導(dǎo)演姓名的函數(shù)。請(qǐng)注意 director 屬性可能會(huì)丟失:
- function getDirector(movie) {
- if (movie.director != null) {
- return movie.director.name;
- }
- }
- getDirector(movieSmall); // => undefined
- getDirector(movieFull); // => 'Ridley Scott'
if (movie.director) {...} 條件用于驗(yàn)證是否定義了 director 屬性。如果沒(méi)有這種預(yù)防措施,則在訪問(wèn)movieSmall 對(duì)象的導(dǎo)演的時(shí),JavaScript 會(huì)引發(fā)錯(cuò)誤 TypeError: Cannot read property 'name' of undefined。
這是用了可選鏈功能并刪除 movie.director 存在驗(yàn)證的正確位置。新版本的 getDirector() 看起來(lái)要短得多:
- function getDirector(movie) {
- return movie.director?.name;
- }
- getDirector(movieSmall); // => undefined
- getDirector(movieFull); // => 'Ridley Scott'
在 movie.director?.name 表達(dá)式中,你可以找到 ?.:可選鏈運(yùn)算符。
對(duì)于 movieSmall,缺少屬性 director。結(jié)果 movie.director?.name 的計(jì)算結(jié)果為 undefined??蛇x鏈運(yùn)算符可防止引發(fā) TypeError: Cannot read property 'name' of undefined 錯(cuò)誤。
相反 movieFull 的屬性 director是可用的。 movie.director?.name 默認(rèn)被評(píng)估為 'Ridley Scott'。
簡(jiǎn)而言之,代碼片段:
- let name = movie.director?.name;
等效于:
- let name;
- if (movie.director != null) {
- name = movie.director.name;
- }
?. 通過(guò)減少兩行代碼簡(jiǎn)化了 getDirector() 函數(shù)。這就是為什么我喜歡可選鏈的原因。
2.1 數(shù)組項(xiàng)
可選鏈能還可以做更多的事。你可以在同一表達(dá)式中自由使用多個(gè)可選鏈運(yùn)算符。甚至可以用它安全地訪問(wèn)數(shù)組項(xiàng)!
下一個(gè)任務(wù)編寫(xiě)一個(gè)返回電影主角姓名的函數(shù)。
在電影對(duì)象內(nèi)部,actor 數(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 屬性,并且該屬性中至少有一個(gè) actor。
使用可選鏈,這個(gè)任務(wù)就很容易解決:
- function getLeadingActor(movie) {
- return movie.actors?.[0]?.name;
- }
- getLeadingActor(movieSmall); // => undefined
- getLeadingActor(movieFull); // => 'Harrison Ford'
actors?. 確保 actors 屬性存在。 [0]?. 確保列表中存在第一個(gè)參與者。這真是個(gè)好東西!
3. 默認(rèn)為Nullish合并
一項(xiàng)名為nullish 合并運(yùn)算符的新提案會(huì)處理 undefined 或 null ,將其默認(rèn)設(shè)置為特定值。
如果 variable 是 undefined 或 null,則表達(dá)式 variable ?? defaultValue 的結(jié)果為 defaultValue。否則,表達(dá)式的計(jì)算結(jié)果為 variable 值。
- const noValue = undefined;
- const value = 'Hello';
- noValue ?? 'Nothing'; // => 'Nothing'
- value ?? 'Nothing'; // => 'Hello'
當(dāng)鏈評(píng)估為 undefined 時(shí),通過(guò)將默認(rèn)值設(shè)置為零,Nullish 合并可以改善可選鏈。
例如,讓我們更改 getLeading() 函數(shù),以在電影對(duì)象中沒(méi)有演員時(shí)返回 "Unknown actor" :
- function getLeadingActor(movie) {
- return movie.actors?.[0]?.name ?? 'Unknown actor';
- }
- getLeadingActor(movieSmall); // => 'Unknown actor'
- getLeadingActor(movieFull); // => 'Harrison Ford'
4. 可選鏈的3種形式
你可以通過(guò)以下 3 種形式使用可選鏈。
第一種形式的 object.property 用于訪問(wèn)靜態(tài)屬性:
- const object = null;
- object?.property; // => undefined
第二種形式 object?.[expression] 用于訪問(wèn)動(dòng)態(tài)屬性或數(shù)組項(xiàng):
- const object = null;
- const name = 'property';
- object?.[name]; // => undefined
- const array = null;
- array?.[0]; // => undefined
最后,第三種形式 object?.([arg1, [arg2, ...]]) 執(zhí)行一個(gè)對(duì)象方法:
- const object = null;
- object?.method('Some value'); // => undefined
如果需要,可以將這些形式組合起來(lái)以創(chuàng)建長(zhǎng)的可選鏈:
- const value = object.maybeUndefinedProp?.maybeNull()?.[propName];
5.短路:在null/undefined 處停止
可選鏈運(yùn)算符的有趣之處在于,一旦在其左側(cè) leftHandSide?.rightHandSide 上遇到空值,就會(huì)停止對(duì)右側(cè)訪問(wèn)器的評(píng)估。這稱為短路。
看一個(gè)例子:
- const nothing = null;
- let index = 0;
- nothing?.[index++]; // => undefined
- index; // => 0
nothing 保留一個(gè)空值,因此可選鏈立即求值為 undefined,并跳過(guò)右側(cè)訪問(wèn)器的求值。因?yàn)?index 的值沒(méi)有增加。
6. 何時(shí)使用可選鏈
要抵制使用可選鏈運(yùn)算符訪問(wèn)任何類(lèi)型屬性的沖動(dòng):這會(huì)導(dǎo)致錯(cuò)誤的用法。下一節(jié)將說(shuō)明何時(shí)正確使用它。
6.1 可能無(wú)效的訪問(wèn)屬性
必須僅在可能為空的屬性附近使用 ?.: maybeNullish?.prop。在其他情況下,請(qǐng)使用老式的屬性訪問(wèn)器:.property 或 [propExpression]。
調(diào)用電影對(duì)象。查看表達(dá)式 movie.director?.name,因?yàn)?director 可以是 undefined,所以在 director 屬性附近使用可選鏈運(yùn)算符是正確的。
相反,使用 ?. 訪問(wèn)電影標(biāo)題 movie?.title 沒(méi)有任何意義。電影對(duì)象不會(huì)是空的。
- // 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 屬性的樣式對(duì)象。 padding 具有可選的屬性 left,top,right,bottom。
嘗試用可選鏈運(yùn)算符:
- 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ù)可以正確地確定元素是否具有填充,但是為每個(gè)屬性使用可選鏈?zhǔn)呛翢o(wú)必要的。
更好的方法是使用對(duì)象散布運(yùn)算符將填充對(duì)象默認(rèn)為零值:
- 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
我認(rèn)為這一版本的 hasPadding() 可讀性更好。
7. 我為什么喜歡它?
我喜歡可選鏈運(yùn)算符,因?yàn)樗试S輕松地從嵌套對(duì)象中訪問(wèn)屬性。它可以防止編寫(xiě)針對(duì)訪問(wèn)者鏈中每個(gè)屬性訪問(wèn)器上的空值進(jìn)行驗(yàn)證的樣板代碼。
當(dāng)可選鏈與空值合并運(yùn)算符結(jié)合使用時(shí),可以得到更好的結(jié)果,從而更輕松地處理默認(rèn)值。