1. 程式人生 > >ES2020 系列:可選鏈 "?." 為啥出現,我們能用它來幹啥?

ES2020 系列:可選鏈 "?." 為啥出現,我們能用它來幹啥?

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/18d583c0d4084119a03ba18e1e8b99a6~tplv-k3u1fbpfcp-zoom-1.image) # 可選鏈 "?." 可選鏈 `?.` 是一種訪問巢狀物件屬性的安全的方式。即使中間的屬性不存在,也不會出現錯誤。 ## “不存在的屬性”的問題 如果你才剛開始讀此教程並學習 JavaScript,那可能還沒接觸到這個問題,但它卻相當常見。 舉個例子,假設我們有很多個 `user` 物件,其中儲存了我們的使用者資料。 我們大多數使用者的地址都儲存在 `user.address` 中,街道地址儲存在 `user.address.street` 中,但有些使用者沒有提供這些資訊。 在這種情況下,當我們嘗試獲取 `user.address.street`,而該使用者恰好沒提供地址資訊,我們則會收到一個錯誤: ```js let user = {}; // 一個沒有 "address" 屬性的 user 物件 alert(user.address.street); // Error! ``` 這是預期的結果。JavaScript 的工作原理就是這樣的。因為 `user.address` 為 `undefined`,嘗試讀取 `user.address.street` 會失敗,並收到一個錯誤。 但是在很多實際場景中,我們更希望得到的是 `undefined`(表示沒有 `street` 屬性)而不是一個錯誤。 ……還有另一個例子。在 Web 開發中,我們可以使用特殊的方法呼叫(例如 `document.querySelector('.elem')`)以物件的形式獲取一個網頁元素,如果沒有這種物件,則返回 `null`。 ```js run // 如果 document.querySelector('.elem') 的結果為 null,則這裡不存在這個元素 let html = document.querySelector('.elem').innerHTML; // 如果 document.querySelector('.elem') 的結果為 null,則會出現錯誤 ``` 同樣,如果該元素不存在,則訪問 `null` 的 `.innerHTML` 時會出錯。在某些情況下,當元素的缺失是沒問題的時候,我們希望避免出現這種錯誤,而是接受 `html = null` 作為結果。 我們如何實現這一點呢? 可能最先想到的方案是在訪問該值的屬性之前,使用 `if` 或條件運算子 `?` 對該值進行檢查,像這樣: ```js let user = {}; alert(user.address ? user.address.street : undefined); ``` 這樣可以,這裡就不會出現錯誤了……但是不夠優雅。就像你所看到的,`"user.address"` 在程式碼中出現了兩次。對於巢狀層次更深的屬性就會出現更多次這樣的重複,這就是問題了。 例如,讓我們嘗試獲取 `user.address.street.name`。 我們既需要檢查 `user.address`,又需要檢查 `user.address.street`: ```js let user = {}; // user 沒有 address 屬性 alert(user.address ? user.address.street ? user.address.street.name : null : null); ``` 這樣就太扯淡了,並且這可能導致寫出來的程式碼很難讓別人理解。 甚至我們可以先忽略這個問題,因為我們有一種更好的實現方式,就是使用 `&&` 運算子: ```js let user = {}; // user 沒有 address 屬性 alert( user.address && user.address.street && user.address.street.name ); // undefined(不報錯) ``` 依次對整條路徑上的屬性使用與運算進行判斷,以確保所有節點是存在的(如果不存在,則停止計算),但仍然不夠優雅。 就像你所看到的,在程式碼中我們仍然重複寫了好幾遍物件屬性名。例如在上面的程式碼中,`user.address` 被重複寫了三遍。 這就是為什麼可選鏈 `?.` 被加入到了 JavaScript 這門程式語言中。那就是徹底地解決以上所有問題! ## 可選鏈 如果可選鏈 `?.` 前面的部分是 `undefined` 或者 `null`,它會停止運算並返回該部分。 **為了簡明起見,在本文接下來的內容中,我們會說如果一個屬性既不是 `null` 也不是 `undefined`,那麼它就“存在”。** 換句話說,例如 `value?.prop`: - 如果 `value` 存在,則結果與 `value.prop` 相同, - 否則(當 `value` 為 `undefined/null` 時)則返回 `undefined`。 下面這是一種使用 `?.` 安全地訪問 `user.address.street` 的方式: ```js let user = {}; // user 沒有 address 屬性 alert( user?.address?.street ); // undefined(不報錯) ``` 程式碼簡潔明瞭,也不用重複寫好幾遍屬性名。 即使 物件 `user` 不存在,使用 `user?.address` 來讀取地址也沒問題: ```js let user = null; alert( user?.address ); // undefined alert( user?.address.street ); // undefined ``` 請注意:`?.` 語法使其前面的值成為可選值,但不會對其後面的起作用。 例如,在 `user?.address.street.name` 中,`?.` 允許 `user` 為 `null/undefined`,但僅此而已。更深層次的屬性是通過常規方式訪問的。如果我們希望它們中的一些也是可選的,那麼我們需要使用更多的 `?.` 來替換 `.`。 > **不要過度使用可選鏈:** > > 我們應該只將 `?.` 使用在一些東西可以不存在的地方。 > > 例如,如果根據我們的程式碼邏輯,`user` 物件必須存在,但 `address` 是可選的,那麼我們應該這樣寫 `user.address?.street`,而不是這樣 `user?.address?.street`。 > > 所以,如果 `user` 恰巧因為失誤變為 undefined,我們會看到一個程式設計錯誤並修復它。否則,程式碼中的錯誤在不恰當的地方被消除了,這會導致除錯更加困難。 > 可選鏈 **`?.` 前的變數必須已宣告** > > 如果未宣告變數 `user`,那麼 `user?.anything` 會觸發一個錯誤: > > ```js > // ReferenceError: user is not defined > user?.address; > ``` `?.` 前的變數必須已宣告(例如 `let/const/var user` 或作為一個函式引數)。可選鏈僅適用於已宣告的變數。 ## 短路效應 正如前面所說的,如果 `?.` 左邊部分不存在,就會立即停止運算(“短路效應”)。 所以,如果後面有任何函式呼叫或者副作用,它們均不會執行。 例如: ```js let user = null; let x = 0; user?.sayHi(x++); // 沒有 "sayHi",因此程式碼執行沒有觸達 x++ alert(x); // 0,值沒有增加 ``` ## 其它變體:?.(),?.[] 可選鏈 `?.` 不是一個運算子,而是一個特殊的語法結構。它還可以與函式和方括號一起使用。 例如,將 `?.()` 用於呼叫一個可能不存在的函式。 在下面這段程式碼中,有些使用者具有 `admin` 方法,而有些沒有: ```js let userAdmin = { admin() { alert("I am admin"); } }; let userGuest = {}; userAdmin.admin?.(); // I am admin userGuest.admin?.(); // 啥都沒有(沒有這樣的方法) ``` 在這兩行程式碼中,我們首先使用點符號(`user1.admin`)來獲取 `admin` 屬性,因為使用者物件一定存在,因此可以安全地讀取它。 然後 `?.()` 會檢查它左邊的部分:如果 `admin` 函式存在,那麼就呼叫執行它(對於 `user1`)。否則(對於 `user2`)運算停止,沒有錯誤。 如果我們想使用方括號 `[]` 而不是點符號 `.` 來訪問屬性,語法 `?.[]` 也可以使用。跟前面的例子類似,它允許從一個可能不存在的物件上安全地讀取屬性。 ```js let user1 = { firstName: "John" }; let user2 = null; // 假設,我們不能授權此使用者 let key = "firstName"; alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined alert( user1?.[key]?.something?.not?.existing); // undefined ``` 此外,我們還可以將 `?.` 跟 `delete` 一起使用: ```js delete user?.name; // 如果 user 存在,則刪除 user.name ``` > **我們可以使用 `?.` 來安全地讀取或刪除,但不能寫入:** > > 可選鏈 `?.` 不能用在賦值語句的左側。 > > 例如: > > ```js > let user = null; > > user?.name = "John"; // Error,不起作用 > // 因為它在計算的是 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`,如果不是則繼續運算。 `?.` 鏈使我們能夠安全地訪問巢狀屬性。 但是,我們應該謹慎地使用 `?.`,僅在當左邊部分不存在也沒問題的情況下使用為宜。以保證在程式碼中有程式設計上的錯誤出現時,也不會對我們隱藏。 --- > 現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。[React 官方文件推薦,與 MDN 並列的 JavaScript 學習教程](https://zh-hans.reactjs.org/docs/getting-started.html#javascript-resources)。 > > 線上免費閱讀:https://zh.javascript.info --- **微信掃描下方二維碼,關注公眾號「技術漫談」,訂閱更多精彩內容。** ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d62b21deb3b47f7bd7c610d3c658da2~tplv-k3u1fbpfcp-zoom-1.image)