我猜各位 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
复制代码
最后但愿本篇文章能对你们有帮助,同时欢迎👏你们关注个人专栏:前端路漫漫。