2022 年 4 月 14 日

可選鏈接「?.」

最近新增
這是最近新增到語言中的功能。舊瀏覽器可能需要多重載入

可選鏈接 ?. 是一種安全的方式來存取巢狀物件屬性,即使中間屬性不存在。

「不存在的屬性」問題

如果您剛開始閱讀教學課程並學習 JavaScript,也許您還沒有遇到這個問題,但它很常見。

舉例來說,假設我們有 user 物件,其中包含了我們的使用者資訊。

我們的大部分使用者都有位址,儲存在 user.address 屬性中,其中包含了街道 user.address.street,但有些使用者沒有提供位址。

在這種情況下,當我們嘗試取得 user.address.street,而使用者恰好沒有提供位址時,我們會得到一個錯誤

let user = {}; // a user without "address" property

alert(user.address.street); // Error!

這是預期的結果。JavaScript 就是這樣運作的。由於 user.addressundefined,因此嘗試取得 user.address.street 會失敗並產生錯誤。

在許多實際情況中,我們會希望在此處取得 undefined 而不是錯誤(表示「沒有街道」)。

…另一個範例。在網頁開發中,我們可以使用特殊的方法呼叫來取得對應於網頁元素的物件,例如 document.querySelector('.elem'),當沒有這樣的元素時,它會傳回 null

// document.querySelector('.elem') is null if there's no element
let html = document.querySelector('.elem').innerHTML; // error if it's null

同樣地,如果元素不存在,我們會在存取 null.innerHTML 屬性時得到一個錯誤。在某些情況下,當元素不存在是正常的,我們希望避免錯誤,並直接接受 html = null 作為結果。

我們該如何做到這一點?

顯而易見的解決方案是在存取其屬性之前,使用 if 或條件運算子 ? 檢查值,如下所示

let user = {};

alert(user.address ? user.address.street : undefined);

它可以運作,不會產生錯誤…但它並不好看。正如你所見,"user.address" 在程式碼中出現了兩次。

以下是 document.querySelector 的相同寫法

let html = document.querySelector('.elem') ? document.querySelector('.elem').innerHTML : null;

我們可以看到,元素搜尋 document.querySelector('.elem') 在這裡實際上被呼叫了兩次。這並不好。

對於更深層巢狀的屬性,它會變得更醜陋,因為需要更多的重複。

例如,讓我們以類似的方式取得 user.address.street.name

let user = {}; // user has no address

alert(user.address ? user.address.street ? user.address.street.name : null : null);

這很糟糕,甚至可能讓人難以理解這樣的程式碼。

有一種更好的寫法,使用 && 運算子

let user = {}; // user has no address

alert( user.address && user.address.street && user.address.street.name ); // undefined (no error)

對整個屬性路徑進行 AND 運算,可以確保所有組件都存在(如果不存在,則評估會停止),但這也不是理想的。

正如你所見,屬性名稱仍然在程式碼中重複。例如,在上面的程式碼中,user.address 出現了三次。

這就是為什麼選擇性串接 ?. 被新增到語言中。一次解決這個問題!

選擇性串接

如果 ?. 前面的值是 undefinednull,選擇性串接 ?. 會停止評估並傳回 undefined

為了簡潔起見,在本文的後續部分,如果某個東西不是 null 也不是 undefined,我們會說它「存在」。

換句話說,value?.prop

  • 如果 value 存在,則會像 value.prop 一樣運作,
  • 否則(當 valueundefined/null)會傳回 undefined

以下是使用 ?. 安全存取 user.address.street 的方法

let user = {}; // user has no address

alert( user?.address?.street ); // undefined (no error)

程式碼簡潔,而且完全沒有重複。

以下是用 document.querySelector 的範例

let html = document.querySelector('.elem')?.innerHTML; // will be undefined, if there's no element

即使 user 物件不存在,也可以使用 user?.address 讀取地址

let user = null;

alert( user?.address ); // undefined
alert( user?.address.street ); // undefined

請注意:?. 語法會讓它前面的值變成可選的,但不會影響後面的值。

例如,在 user?.address.street.name 中,?. 允許 user 安全地為 null/undefined(並在這種情況下傳回 undefined),但這只適用於 user。後面的屬性會以一般方式存取。如果我們希望其中一些屬性變成可選的,則需要將更多的 . 替換為 ?.

不要過度使用可選鍊接

我們應該只在可以接受某些東西不存在的情況下使用 ?.

例如,如果根據我們的程式碼邏輯,user 物件必須存在,但 address 是可選的,則我們應該寫 user.address?.street,而不是 user?.address?.street

然後,如果 user 恰好是未定義的,我們會看到一個關於它的程式設計錯誤並修復它。否則,如果我們過度使用 ?.,編碼錯誤可能會在不適當的地方被隱藏,並變得更難除錯。

?. 前面的變數必須宣告

如果根本沒有 user 變數,則 user?.anything 會觸發錯誤

// ReferenceError: user is not defined
user?.address;

必須宣告變數(例如 let/const/var user 或作為函式參數)。可選鍊接僅適用於已宣告的變數。

短路

如前所述,如果左側部分不存在,?. 會立即停止(「短路」)評估。

因此,如果在 ?. 的右側有任何進一步的函式呼叫或運算,則不會執行這些呼叫或運算。

例如

let user = null;
let x = 0;

user?.sayHi(x++); // no "user", so the execution doesn't reach sayHi call and x++

alert(x); // 0, value not incremented

其他變體:?.()、?.[]

可選鍊接 ?. 不是運算子,而是一種特殊的語法結構,也可以用於函式和方括號。

例如,?.() 用於呼叫可能不存在的函式。

在以下程式碼中,我們的一些使用者有 admin 方法,而另一些使用者沒有

let userAdmin = {
  admin() {
    alert("I am admin");
  }
};

let userGuest = {};

userAdmin.admin?.(); // I am admin

userGuest.admin?.(); // nothing happens (no such method)

在這裡,在兩行中,我們首先使用點(userAdmin.admin)來取得 admin 屬性,因為我們假設 user 物件存在,因此可以安全地從中讀取。

然後 ?.() 檢查左側部分:如果 admin 函式存在,則會執行(對於 userAdmin 就是如此)。否則(對於 userGuest),評估會在沒有錯誤的情況下停止。

如果我們想使用方括號 [] 來存取屬性,而不是點 .,則 ?.[] 語法也可以使用。與前述情況類似,它允許安全地從可能不存在的物件中讀取屬性。

let key = "firstName";

let user1 = {
  firstName: "John"
};

let user2 = null;

alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined

我們也可以將 ?.delete 搭配使用

delete user?.name; // delete user.name if user exists
我們可以使用 ?. 來安全地讀取和刪除,但不能寫入

選擇性連鎖 ?. 在賦值運算的左側沒有作用。

例如

let user = null;

user?.name = "John"; // Error, doesn't work
// because it evaluates to: undefined = "John"

摘要

選擇性連鎖 ?. 語法有三個形式

  1. obj?.prop – 如果 obj 存在,則傳回 obj.prop,否則傳回 undefined
  2. obj?.[prop] – 如果 obj 存在,則傳回 obj[prop],否則傳回 undefined
  3. obj.method?.() – 如果 obj.method 存在,則呼叫 obj.method(),否則傳回 undefined

正如我們所見,所有這些都非常直接且易於使用。?. 檢查左側是否存在 null/undefined,如果不存在,則允許評估繼續進行。

?. 的鏈條允許安全地存取巢狀屬性。

儘管如此,我們應該小心地套用 ?.,僅在根據我們的程式碼邏輯,左側不存在是可以接受的情況下才套用。這樣一來,如果發生程式設計錯誤,它就不會向我們隱藏這些錯誤。

教學課程地圖

留言

留言前請先閱讀這段文字…
  • 如果您有改善建議,請 提交 GitHub 議題 或提交 Pull Request,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行程式碼,請將它們包覆在 <pre> 標籤中,若要插入超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)