Path
深层路径工具:解析 name path、读取 / 写入 / 删除嵌套值,并提供路径标记、路径收集与安全 key 保护
概述
import { deepGet, deepSet, deepUnset, keyOfName, toPathArray, toSegments, unflatten } from '@skyroc/utils';path 模块提供一组面向对象 / 数组深层字段的工具,主要用于表单状态、字段依赖、脏标记和嵌套数据转换。
常规使用直接从 @skyroc/utils 主入口导入即可。主入口同时 re-export radash,因此路径读写函数使用 deepGet、deepSet、deepUnset、unflatten 这组名字,避免和 radash 的 get、set、construct 语义冲突。
路径表示
路径工具统一使用 NamePath 表示字段路径:
type Key = string | number;
type PathTuple = readonly Key[];
type NamePath = Key | PathTuple | undefined;常见写法:
| 写法 | 解析结果 | 说明 |
|---|---|---|
'profile.name' | ['profile', 'name'] | 点路径 |
'items[0].title' | ['items', 0, 'title'] | 数组下标会转成 number |
'items["sku.code"]' | ['items', 'sku.code'] | 带点的 key 可用引号括起来 |
['profile', 'name'] | ['profile', 'name'] | tuple 路径会复制一份 |
0 | [0] | 单个 number key |
toPathArray(path)
将字符串路径解析为数组片段。
toPathArray('user.addresses[0].city');
// ['user', 'addresses', 0, 'city']
toPathArray('items["sku.code"]');
// ['items', 'sku.code']
toPathArray("items['sku.code']");
// ['items', 'sku.code']
toPathArray('items[01]');
// ['items', '01'],非规范数字下标保留为字符串toSegments(path)
将 NamePath 规范化为路径片段数组。
toSegments('user[0]');
// ['user', 0]
toSegments(['user', 0]);
// ['user', 0]
toSegments(0);
// [0]业务代码里通常传字符串、number 或 tuple。deepGet 会把 null / undefined 当作空路径并返回默认值;deepSet / deepUnset 如果需要明确 no-op,传空数组 []。
keyOfTuple(tuple) / keyOfName(name)
将路径片段拼成稳定的点路径字符串,适合做 Set / Map 的 key。
keyOfTuple(['profile', 'name']);
// 'profile.name'
keyOfName('profile.name');
// 'profile.name'
keyOfName(['profile', 'name']);
// 'profile.name'读取值
deepGet(obj, path, def?)
安全读取嵌套值。路径不存在、路径为空、读取过程中遇到非对象值,都会返回默认值。
import { deepGet } from '@skyroc/utils';
const source = {
items: [{ title: 'First' }],
profile: {
age: undefined,
name: 'Alex',
nullable: null,
},
};
deepGet(source, 'profile.name');
// 'Alex'
deepGet(source, ['items', 0, 'title']);
// 'First'
deepGet(source, 'items[0].title');
// 'First'
deepGet(source, 'profile.missing', 'fallback');
// 'fallback'
deepGet(source, 'profile.age', 'fallback');
// 'fallback'
deepGet(source, 'profile.nullable', 'fallback');
// null,显式 null 会被保留deepGet 对 undefined 和 null 的语义不同:
| 值 | 结果 |
|---|---|
目标值是 undefined | 返回默认值 |
目标值是 null | 返回 null |
path 是 null / undefined / [] | 返回默认值 |
写入值
deepSet(obj, path, value, options?)
不可变写入嵌套值。原对象不会被修改;沿路径经过的对象 / 数组会被复制,缺失的中间容器会按路径片段自动创建。
import { deepSet } from '@skyroc/utils';
const source = { profile: { name: 'Alex' } };
const next = deepSet(source, 'profile.age', 18);
next;
// { profile: { name: 'Alex', age: 18 } }
source;
// { profile: { name: 'Alex' } }数字路径片段会创建数组容器:
deepSet({}, 'items[0].title', 'First');
// { items: [{ title: 'First' }] }
deepSet(undefined, [0, 'title'], 'First');
// [{ title: 'First' }]空路径会直接返回原值:
const source = { profile: { name: 'Alex' } };
deepSet(source, [], 'Next') === source;
// truedeepUnset(obj, path, options?)
不可变删除嵌套值。删除对象字段时会移除 key;删除数组下标时会使用 splice,后续元素会前移。
import { deepUnset } from '@skyroc/utils';
const source = {
items: ['first', 'second'],
profile: { age: 18, name: 'Alex' },
};
deepUnset(source, 'profile.age');
// { items: ['first', 'second'], profile: { name: 'Alex' } }
deepUnset(source, ['items', 0]);
// { items: ['second'], profile: { age: 18, name: 'Alex' } }当根值不是对象 / 数组,或路径为空时,deepUnset 不会创建新结构:
deepUnset(1, 'profile.name');
// 1
deepUnset(source, []) === source;
// true安全 key
deepSet 和 deepUnset 默认开启 safeKeys,会阻止以下危险 key:
__proto__constructorprototype
deepSet({}, '__proto__.polluted', true);
// {}
({} as Record<string, unknown>).polluted;
// undefined如确实需要把这些字符串当作普通业务字段写入,可以显式关闭:
deepSet({}, 'constructor.value', 1, { safeKeys: false });
// { constructor: { value: 1 } }扁平对象转嵌套对象
unflatten(obj)
将以路径字符串为 key 的扁平对象展开为嵌套结构,适合把表单字段快照、查询结果或补丁对象还原为业务对象。
import { unflatten } from '@skyroc/utils';
unflatten({
'items[0].title': 'First',
'profile.name': 'Alex',
});
// {
// items: [{ title: 'First' }],
// profile: { name: 'Alex' },
// }传入空值时返回空对象:
unflatten(null as any);
// {}路径状态标记
以下函数用于把字段路径规范化为字符串 key 后写入 Set<string>,常见于 dirty、touched、validating 等字段状态集合。
import { allOn, anyOn, flagOff, flagOn, isOn } from '@skyroc/utils';
const flags = new Set<string>();
flagOn(flags, ['profile', 'name']);
isOn(flags, 'profile.name');
// true
anyOn(flags, [['profile', 'name']]);
// true
allOn(flags, [['profile', 'name']]);
// true
flagOff(flags, 'profile.name');
isOn(flags, ['profile', 'name']);
// falseanyOn(set) 和 allOn(set) 在没有传 names 时,都会退化为判断集合是否非空。
路径收集
collectChangedLeafPaths(input)
收集对象里的叶子路径。数组节点本身也会作为一条变更路径返回,这样上层订阅数组字段时可以收到数组结构变化。
collectChangedLeafPaths({
items: [{ title: 'First' }],
profile: { name: 'Alex' },
});
// [
// ['items'],
// ['items', '0', 'title'],
// ['profile', 'name'],
// ]unionPaths(a, b)
合并两组路径,按第一次出现的顺序去重。
unionPaths(
[
['profile', 'name'],
['items', 0],
],
[
['items', 0],
['profile', 'age'],
]
);
// [
// ['profile', 'name'],
// ['items', 0],
// ['profile', 'age'],
// ]isUnderPrefix(key, prefix)
判断路径是否落在某个前缀下面,支持精确匹配、点路径子级匹配,以及 * / 空字符串通配。
isUnderPrefix('profile.name', 'profile');
// true
isUnderPrefix('profile', 'profile');
// true
isUnderPrefix('profileName', 'profile');
// false
isUnderPrefix('profile.name', '*');
// truecollectDeepKeys(obj)
递归收集深层叶子 key。null、undefined、非对象值和 Date 会被视为叶子;空对象会返回当前路径。
collectDeepKeys({
empty: {},
list: [1],
nil: null,
profile: {
birthday: new Date('2026-05-07T00:00:00.000Z'),
name: 'Alex',
},
});
// [
// 'empty',
// 'list.0',
// 'nil',
// 'profile.birthday',
// 'profile.name',
// ]类型守卫与容器工具
这些工具主要供表单内核和路径写入逻辑复用。
| 函数 | 说明 |
|---|---|
isPlainObject(value) | 判断是否为普通对象,数组会返回 false |
isObjectRecord(value) | 判断是否为非 null 对象,数组也会返回 true |
isObjectLike(value) | 判断是否为非 null 对象 |
isUnsafeKey(key) | 判断 key 是否为 __proto__、constructor 或 prototype |
emptyContainer(nextKey) | nextKey 是 number 时返回 [],否则返回 {} |
微任务调度
microtask(cb)
优先使用 queueMicrotask,不可用时降级为 Promise.resolve().then(cb)。
import { microtask } from '@skyroc/utils';
microtask(() => {
// 在当前同步任务结束后执行
});