deepGet / get:安全访问嵌套对象的终极方案
在 JavaScript 开发中,访问深层嵌套对象属性是高频需求,但也是 Cannot read property 'x' of undefined 错误的主要来源。
传统防御性写法冗长且难以维护,而 deepGet(或 get)函数通过路径字符串或数组,提供了一种简洁、安全、可复用的解决方案。
它不仅是“语法糖”,更是处理不确定数据结构的必要工具。
问题:嵌套访问的脆弱性
假设我们有用户数据:
ts
const user = {
profile: {
address: {
city: 'Beijing'
}
}
}直接访问:
ts
const city = user.profile.address.city // 'Beijing'但若 user 来自 API,可能缺少字段:
ts
const user = { profile: null }
const city = user.profile.address.city // ❌ TypeError: Cannot read property 'address' of null传统写法:冗长的防御链
方案 1:逻辑与操作符(&&)
ts
const city = user && user.profile && user.profile.address && user.profile.address.city- 优点:无需额外依赖。
- 缺点:代码重复、路径越深越难读、无法复用。
方案 2:可选链(?.)
ts
const city = user?.profile?.address?.city- 优点:ES2020 原生支持,简洁。
- 缺点:仅支持静态路径,无法动态传入路径字符串;旧浏览器需转译。
注意:可选链是 deepGet 的有力竞争者,但两者适用场景不同:
?.适合静态路径的直接访问。deepGet适合动态路径(如配置、表单校验规则)或需要默认值的场景。
deepGet 设计:路径支持与默认值
核心接口
ts
const deepGet = <T, D = undefined>(
obj: T,
path: string | string[],
defaultValue?: D
): unknown => { /* 实现 */ }使用示例
ts
// 路径字符串
deepGet(user, 'profile.address.city', 'Unknown')
// 路径数组(性能更优)
deepGet(user, ['profile', 'address', 'city'], 'Unknown')
// 路径不存在,返回默认值
deepGet(user, 'profile.phone.number', 'N/A') // 'N/A'实现:递归遍历路径
ts
const deepGet = <T, D = undefined>(
obj: T,
path: string | string[],
defaultValue?: D
): unknown => {
// 处理空对象
if (obj == null) return defaultValue
// 统一为数组
const paths = Array.isArray(path) ? path : path.split('.')
let current: any = obj
for (const key of paths) {
// 任一环节为 null/undefined,中断并返回默认值
if (current == null) {
return defaultValue
}
current = current[key]
}
// 若最终值为 undefined,返回默认值
return current !== undefined ? current : defaultValue
}关键逻辑
- 提前终止:一旦
current为null或undefined,立即返回defaultValue。 - 路径分割:字符串路径通过
split('.')转为数组。 - 默认值兜底:即使路径存在,但值为
undefined,也返回defaultValue。
性能优化:路径字符串 vs 数组
问题:字符串分割的开销
ts
deepGet(obj, 'a.b.c.d') // 每次调用都执行 split('.')在高频调用场景(如渲染列表),split 的性能开销不可忽视。
解法:优先使用数组路径
ts
// 推荐:路径数组,避免重复分割
deepGet(obj, ['a', 'b', 'c', 'd'])若必须使用字符串,可考虑缓存分割结果,但会增加复杂度。在大多数场景下,直接传数组是更优选择。
类型安全:精确推导路径返回类型
理想情况下,我们希望:
ts
const city = deepGet(user, 'profile.address.city')
// city 的类型应自动推导为 string | undefined这需要高级类型系统支持。
工具类型:DotPath 与 PathValue
1. 递归解析路径字符串
ts
// 将 'a.b.c' 转为 'a' | 'b' | 'c' 的联合类型
type Split<S extends string> = S extends `${infer T}.${infer U}`
? T | Split<U>
: S
// 从对象 T 中,根据路径 P 获取值类型
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Rest extends keyof T[K]
? T[K][Rest] // 直接命中
: T[K] extends Record<string, any>
? PathValue<T[K], Rest> // 递归深入
: never
: never
: P extends keyof T
? T[P]
: never2. 完整类型定义
ts
declare function deepGet<T, P extends string>(
obj: T,
path: P,
defaultValue?: PathValue<T, P>
): PathValue<T, P> | undefined
declare function deepGet<T, P extends (keyof T)[], D>(
obj: T,
path: P,
defaultValue: D
): D | PathValue<T, P[0]> // 简化版,实际需递归推导数组路径挑战:TypeScript 对递归类型的深度有限制(默认 50),复杂路径可能导致 Type instantiation is excessively deep 错误。生产库(如 lodash)通常采用重载而非完全递归推导。
实战:在 Vue/React 中安全渲染用户信息
text
<script setup lang="ts">
import { computed } from 'vue'
import { deepGet } from '@/utils/object'
const props = defineProps<{
user: User | null
}>()
// 安全获取用户城市,避免模板中报错
const userCity = computed(() => {
return deepGet(props.user, 'profile.address.city', '未知城市')
})
</script>
<template>
<div>
<p>所在城市:{{ userCity }}</p>
</div>
</template>结语:deepGet 是处理“不确定性”的优雅封装
deepGet 的价值不仅在于避免错误,更在于:
- 声明式:用路径描述访问逻辑,而非过程式
if判断。 - 可复用:一处定义,多处使用。
- 可组合:可与
map、filter等结合处理数据流。 - 类型安全:配合高级类型,提供编译时保障。
当你手写 deepGet 时,你不仅在实现一个函数,更在构建一种处理不完整、不确定数据的思维模式。
在 API 响应不可控、配置结构动态变化的现代应用中,这种能力至关重要。