如何安全地读写深度嵌套的对象?
栏目: JavaScript · 发布时间: 6年前
内容简介:我猜各位 JSer,或多或少都遇到过这种错误:这里有一个深度嵌套的象:我们的
我猜各位 JSer,或多或少都遇到过这种错误: Uncaught TypeError: Cannot read property 'someProp' of undefined
。当我们从 null
或者 undefined
上去读某个属性时,就会报这种错误。尤其一个复杂的前端项目可能会对接各种各样的后端服务,某些服务不可靠,返回的数据并不是约定的格式时,就很容易出现这种错误。
这里有一个深度嵌套的象:
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
信息,你可能会写出下面这样的代码:
const { contact: { email } = {} } = nestedObj // 或者这样 const email2 = (nestedObj.contact || {}).email // 又或者这样 const email3 = nestedObj.contact && nestedObj.contact.email 复制代码
上面做法就是给某些可能不存在的属性加一个默认值或者判断属性是否存在,这样我们就可以安全地读它的属性。这种手动加默认的办法或者判断的方法,在对象嵌套不深的情况下还可以接受,但是当对象嵌套很深时,采用这种方法就会让人崩溃。会写类似这样的代码: const res = a.b && a.b.c && ...
。
读取深度嵌套的对象
下面我们来看看如何读取深度嵌套的对象:
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, 不再报错了:+1: 复制代码
现在我们利用 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
相关的内容:
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
可以非常方便地处理「嵌套型表单」,例子的完整代码在 这里
。
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 复制代码
最后希望本篇文章能对大家有帮助,同时欢迎:clap:大家关注我的专栏:前端路漫漫。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。