如何安全地讀寫深度嵌套的對象?

我猜各位 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 屬性,裏面有對應的 phoneemail,可是可能由於各類各樣緣由(好比:不可靠的服務), contact 並不存在。若是咱們想直接讀取 email 信息,毫無疑問是不能夠的,由於contactundefined。有時你不肯定 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
複製代碼

咱們這裏寫的 assocassocPath 均是 pure function,不會直接修改傳進來的數據。我以前寫了一個庫 js-lens,主要的實現方式就是依賴上面寫的幾個函數,而後加了一些函數式特性,好比 compose。這個庫的實現參考了 ocaml-lensRamda 相關部門的代碼。下面咱們來介紹一下 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 函數來更新對應的屬性,overset 的做用同樣,都是用來更新某個屬性,只不過他的第二個參數是一個函數,該函數的返回值用來更新對應的屬性。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
複製代碼

最後但願本篇文章能對你們有幫助,同時歡迎👏你們關注個人專欄:前端路漫漫

相關文章
相關標籤/搜索