跳至主要内容

可選鏈(Optional chaining)

· 閱讀時間約 5 分鐘
Maya Armyanova ([@Zmayski](https://twitter.com/Zmayski)),可選鏈的破壞者

在 JavaScript 中,長鏈的屬性訪問可能容易出錯,因為其中任何一步都可能評估為 nullundefined(也稱為“空值”)。在每一步進行屬性存在檢查,很容易變成深度嵌套的 if 語句或一個長長的 if 條件,複製屬性訪問的鏈路:

// 容易出錯的版本,可能會拋出錯誤。
const nameLength = db.user.name.length;

// 不容易出錯,但可讀性較差。
let nameLength;
if (db && db.user && db.user.name)
nameLength = db.user.name.length;

上述代碼也可以使用三元運算符來表示,但可讀性並不會提高:

const nameLength =
(db
? (db.user
? (db.user.name
? db.user.name.length
: undefined)
: undefined)
: undefined);

引入可選鏈操作符

顯然你不希望寫那樣的代碼,因此需要一種可行的替代方案。其他一些語言通過使用名為“可選鏈”的功能提供了一個優雅的解決方案。根據最近的規範提案,“可選鏈是一個包含一個或多個屬性訪問和函數調用的鏈路,其第一部分以 ?. 作為標誌開始”。

使用新的可選鏈操作符,我們可以將上述示例重寫如下:

// 同樣檢查錯誤且更加可讀。
const nameLength = db?.user?.name?.length;

dbusernameundefinednull 時會發生什麼?使用可選鏈操作符,JavaScript 將初始化 nameLengthundefined,而不是拋出錯誤。

請注意,此行為也比我們的檢查方式 if (db && db.user && db.user.name) 更加健全。例如,如果 name 始終保證是字符串,我們可以將 name?.length 更改為 name.length。那麼,如果 name 是空字符串,我們仍然可以得到正確的長度 0。這是因為空字符串是一個假值:在 if 條件中其行為類似 false。可選鏈操作符修復了這個常見的錯誤源。

附加的語法形式:方法調用和動態屬性

此外,還有一種適用於調用可選方法的操作符版本:

// 擴展了接口,包含一個僅針對管理員用戶存在的可選方法。
const adminOption = db?.user?.validateAdminAndGetPrefs?.().option;

語法可能感到意外,因為 ?.() 是實際的操作符,應用於它之前的表達式。

操作符的第三種用法即可選的動態屬性訪問,其通過 ?.[] 完成。它返回括號內參數引用的值,或者當沒有對象可用來獲取該值時返回 undefined。以下是一個可能的使用例子,與上面的示例類似:

// 擴展了靜態屬性訪問的功能,並使用動態生成的屬性名稱。
const optionName = 'optional setting';
const optionLength = db?.user?.preferences?.[optionName].length;

這種形式也可以用於可選陣列索引,例如:

// 如果 `usersArray` 為 `null` 或 `undefined`,
// 那麼 `userName` 慎重地評估為 `undefined`。
const userIndex = 42;
const userName = usersArray?.[userIndex].name;

可選鏈操作符可以與空值合併 ?? 操作符結合使用,當需要非 undefined 的默認值時。這允許以指定的默認值安全地訪問深層屬性,解決了一個之前需要使用像 lodash 的 _.get 這樣的第三方庫常見的用例:

const object = { id: 123, names: { first: 'Alice', last: 'Smith' }};

{ // 使用 lodash:
const firstName = _.get(object, 'names.first');
// → 'Alice'

const middleName = _.get(object, 'names.middle', '(no middle name)');
// → '(no middle name)'
}

{ // 使用可選鏈和空值合併:
const firstName = object?.names?.first ?? '(no first name)';
// → 'Alice'

const middleName = object?.names?.middle ?? '(no middle name)';
// → '(no middle name)'
}

可選鏈操作符的特性

可選鏈操作符具有一些有趣的特性:短路堆疊可選刪除。讓我們通過示例逐一講解。

短路 意味著當可選鏈操作符早期返回時不評估表達式的其餘部分:

// `age` 只有在 `db` 和 `user` 已定義時才會自增。
db?.user?.grow(++age);

堆疊(Stacking) 意味著可以在屬性訪問序列中應用多個可選鏈操作符:

// 一個可選鏈操作符可以跟隨另一個可選鏈操作符。
const firstNameLength = db.users?.[42]?.names.first.length;

然而,在單一鏈中使用多於一個可選鏈操作符時要謹慎。如果某個值保證不會是 null 或 undefined,那麼在它上使用 ?. 來訪問屬性是不建議的。以上範例中,db 被認為是始終已定義的,但 db.usersdb.users[42] 可能不是。如果資料庫中存在這樣的使用者,那麼 names.first.length 被認為總是已定義。

可選刪除(Optional deletion) 指的是 delete 操作符可以與可選鏈結合使用:

// 只有在 `db` 已定義時,`db.user` 才會被刪除。
delete db?.user;

更多詳情可以在 提案的 Semantics 部分 中找到。

可選鏈的支持