我猜各位 JSer,或多或少都遇到過這種錯誤: Uncaught TypeError: Cannot read property 'someProp' of undefined
。當咱們從 null
或者 undefined
上去讀某個屬性時,就會報這種錯誤。尤爲一個複雜的前端項目可能會對接各類各樣的後端服務,某些服務不可靠,返回的數據並非約定的格式時,就很容易出現這種錯誤。前端
這裏有一個深度嵌套的象:react
let nestedObj = {
user: {
name: 'Victor',
favoriteColors: ["black", "white", "grey"],
// contact info doesn't appear here
// contact: {
// phone: 123,
// email: "123@example.com"
// }
}
}
複製代碼
咱們的 nestedObj
本應該有一個 contact
屬性,裏面有對應的 phone
和 email
,可是可能由於各類各樣緣由(好比:不可靠的服務), contact
並不存在。若是咱們想直接讀取 email 信息,毫無疑問是不能夠的,由於contact
是 undefined
。有時你不肯定 contact
是否存在, 爲了安全的讀到 email
信息,你可能會寫出下面這樣的代碼:git
const { contact: { email } = {} } = nestedObj
// 或者這樣
const email2 = (nestedObj.contact || {}).email
// 又或者這樣
const email3 = nestedObj.contact && nestedObj.contact.email
複製代碼
上面作法就是給某些可能不存在的屬性加一個默認值或者判斷屬性是否存在,這樣咱們就能夠安全地讀它的屬性。這種手動加默認的辦法或者判斷的方法,在對象嵌套不深的狀況下還能夠接受,可是當對象嵌套很深時,採用這種方法就會讓人崩潰。會寫相似這樣的代碼:const res = a.b && a.b.c && ...
。github
下面咱們來看看如何讀取深度嵌套的對象:後端
const path = (paths, obj) => {
return paths.reduce((val, key) => {
// val 是 null 或者 undefined, 咱們返回undefined,不然的話咱們讀取「下一層」的數據
if (val == null) {
return undefined
}
return val[key]
}, obj)
}
path(["user", "contact", "email"], nestedObj) // 返回undefined, 再也不報錯了👍
複製代碼
如今咱們利用 path
函數能夠安全得讀取深度嵌套的對象了,那麼咱們如何寫入或者更新深度嵌套的對象呢? 這樣確定是不行的 nestedObj.contact.email = 123@example.com
,由於不能在 undefined 上寫入任何屬性。數組
下面咱們來看看如何安全的更新屬性:安全
// assoc 在 x 上添加或者修改一個屬性,返回修改後的對象/數組,不改變傳入的 x
const assoc = (prop, val, x) => {
if (Number.isInteger(prop) && Array.isArray(x)) {
const newX = [...x]
newX[prop] = val
return newX
} else {
return {
...x,
[prop]: val
}
}
}
// 根據提供的 path 和 val,在 obj 上添加或者修改對應的屬性,不改變傳入的 obj
const assocPath = (paths, val, obj) => {
// paths 是 [],返回 val
if (paths.length === 0) {
return val
}
const firstPath = paths[0];
obj = (obj != null) ? obj : (Number.isInteger(firstPath) ? [] : {});
// 退出遞歸
if (paths.length === 1) {
return assoc(firstPath, val, obj);
}
// 藉助上面的 assoc 函數,遞歸地修改 paths 裏包含屬性
return assoc(
firstPath,
assocPath(paths.slice(1), val, obj[firstPath]),
obj
);
};
nestedObj = assocPath(["user", "contact", "email"], "123@example.com", nestedObj)
path(["user", "contact", "email"], nestedObj) // 123@example.com
複製代碼
咱們這裏寫的 assoc
和 assocPath
均是 pure function
,不會直接修改傳進來的數據。我以前寫了一個庫 js-lens
,主要的實現方式就是依賴上面寫的幾個函數,而後加了一些函數式特性,好比 compose
。這個庫的實現參考了 ocaml-lens
和 Ramda
相關部門的代碼。下面咱們來介紹一下 lens
相關的內容:app
const { lensPath, lensCompose, view, set, over } = require('js-lens')
const contactLens = lensPath(['user', 'contact'])
const colorLens = lensPath(['user', 'favoriteColors'])
const emailLens = lensPath(['email'])
const contactEmailLens = lensCompose(contactLens, emailLens)
const thirdColoLens = lensCompose(colorLens, lensPath([2]))
view(contactEmailLens, nestedObj) // undefined
nestedObj = set(contactEmailLens, "123@example.com", nestedObj)
view(contactEmailLens, nestedObj) // "123@example.com"
view(thirdColoLens, nestedObj) // "grey"
nestedObj = over(thirdColoLens, color => "dark " + color, nestedObj)
view(thirdColoLens, nestedObj) // "dark grey"
複製代碼
我來解釋一下上面引用的函數的意思,lensPath
接收 paths
數組,而後會返回一個 getter
和 一個 setter
函數,view
利用返回的 getter
來讀取對應的屬性,set
利用返回的 setter
函數來更新對應的屬性,over
和 set
的做用同樣,都是用來更新某個屬性,只不過他的第二個參數是一個函數,該函數的返回值用來更新對應的屬性。lensCompose
能夠把傳入 lens
compose 起來, 返回一個 getter
和 一個 setter
函數,當咱們數據變得很複雜,嵌套很深的時候,它的做用就很明顯了。函數
下面咱們來看一個例子,利用lens
能夠很是方便地處理「嵌套型表單」,例子的完整代碼在 這裏。ui
import React, { useState } from 'react'
import { lensPath, lensCompose, view, set } from 'js-lens'
const contactLens = lensPath(['user', 'contact'])
const nameLens = lensPath(['user', 'name'])
const emailLens = lensPath(['email'])
const addressLens = lensPath(['addressLens'])
const contactAddressLens = lensCompose(contactLens, addressLens)
const contactEmailLens = lensCompose(contactLens, emailLens)
const NestedForm = () => {
const [data, setData] = useState({})
const value = (lens, defaultValue = '') => view(lens, data) || defaultValue
const update = (lens, v) => setData(prev => set(lens, v, prev))
return (
<form
onSubmit={(e) => {
e.preventDefault()
console.log(data)
}}
>
{JSON.stringify(data)}
<br />
<input
type="text"
placeholder="name"
value={value(nameLens)}
onChange={e => update(nameLens, e.target.value)}
/>
<input
type="text"
placeholder="email"
value={value(contactEmailLens)}
onChange={e => update(contactEmailLens, e.target.value)}
/>
<input
type="text"
placeholder="address"
value={value(contactAddressLens)}
onChange={e => update(contactAddressLens, e.target.value)}
/>
<br />
<button type="submit">submit</button>
</form>
)
}
export default NestedForm
複製代碼
最後但願本篇文章能對你們有幫助,同時歡迎👏你們關注個人專欄:前端路漫漫。