es-toolkit set 函数一篇讲透:路径写入、自动建嵌套、源码级避坑指南
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
把老项目从 lodash 迁到 es-toolkit 时,很多人会卡在这种地方:大部分函数换个 import 就完事,唯独_.set让人犹豫——路径往往是运行时拼出来的(比如_.set(obj, field + '.value', v)),没法直接赋值。
好消息是,es-toolkit set 函数(来自 compat 兼容层)就是 lodash set 替代的现成答案,行为与_.set几乎逐行对齐,可以整段照搬:
import { set } from 'es-toolkit/compat'; const config = {}; set(config, 'server.port', 8080); set(config, ['cache', 'ttl'], 3600);但在你开始全局替换之前,先花三分钟搞清楚它到底怎么工作、坑藏在哪。
es-toolkit compat set 快速上手:三种路径写法
签名一行说清:
set(object, path, value)—— 往object的path位置写value,返回修改后的原对象本身。
path的类型是PropertyPath(定义在 src/compat/_internal/PropertyPath.ts),白话解释:可以是单个键(字符串/数字/symbol),也可以是键的数组,三种写法都能用:
// 点号写法 set(obj, 'a.b.c', 4); // 括号写法 set(obj, 'users[0].name', 'John'); // 数组写法 set(obj, ['nested', 'array', 0], 'first');它的核心能力是按路径写入对象时自动创建嵌套结构:路径中间哪一段不存在,就自动补哪一段;而且补出来的容器类型是智能的——下一段是数组下标就建数组,否则建普通对象。
const empty = {}; set(empty, 'user.profile.name', 'John'); console.log(empty); // { user: { profile: { name: 'John' } } }这正是它和obj.a.b = v拉开差距的地方:直接赋值在这里会直接抛Cannot set properties of undefined。
set 的路径到底怎么拆:toPath 解析规则表
所有字符串路径都会过一遍 src/compat/util/toPath.ts 的解析器。先给结论:点号拆、括号拆、引号里的不拆、数字原样保留。
| 路径写法 | 解析结果 | 规则 |
|---|---|---|
'a.b.c' | ['a', 'b', 'c'] | 点号拆分 |
'a[b][c]' | ['a', 'b', 'c'] | 括号等价于点号 |
a["b.c"].d | ['a', 'b.c', 'd'] | 引号内容不拆 |
'a..b' | ['a', '', 'b'] | 连续点号产生空段 |
'.a.b' | ['', 'a', 'b'] | 前导点号是空键 |
'items[0]' | ['items', '0'] | 下标以字符串保留 |
有两个反直觉的点,es-toolkit 路径解析里最容易理解错:
- 含点号的字符串不一定被拆。
isKey判定它是"普通键"时(比如把'a.b'当单个键名),就不会拆。所以set(obj, 'a.b')和set(obj, ['a.b'])效果相同——都是写一个字面键名为a.b的属性。 - 数组路径不走字符串解析,原样使用。当键本身带点号、空格等特殊字符时,数组写法是最稳的选择。
set 行为避坑清单:原地修改、稀疏数组与 null 安全
这部分是线上最容易出问题的地方。⚠️
原地修改,不是不可变更新。
const original = { x: 1 }; const result = set(original, 'y', 2); original === result; // true,同一个引用set直接改你传进去的对象。如果你的状态管理(Redux、Zustand 之类)靠引用变化触发更新,一次set会让 UI 完全无感。需要不可变语义时,用解构赋值,或者先克隆再写。
跳号下标会造出稀疏数组。
const list = {}; set(list, 'items[0]', 'first'); set(list, 'items[2]', 'third'); console.log(list.items); // ['first', <空位>, 'third']中间下标不会被填undefined,而是留空洞(JSON.stringify会输出null)。下游代码若假定密集数组,遍历和索引都会对不上。
中间节点是基本类型时,直接覆盖。比如set({ a: '' }, 'a.b', 2)会得到{ a: { b: 2 } }——原值被容器对象顶掉,不报错也不提醒。
null 安全。对象参数传null/undefined时不抛错,原样返回。防御性写法很友好,但要注意"写入"其实什么都没发生。
原型污染防线。路径里出现__proto__这类危险键时,函数直接中止并返回原对象,污染不了原型链。这是它比手写obj[key] = value循环多出来的一道安全保险。
updateWith 流水线到底在干什么:set 为什么慢
源码视角看,src/compat/object/set.ts 的全部实现就是 4 行委托:
export function set<T extends object>(obj: T, path: PropertyPath, value: any): T { return updateWith(obj, path, () => value, () => undefined); }也就是说,每次调用的开销全在 src/compat/object/updateWith.ts 的流水线上:
- 路径归一化:先试
isKey判定,再试数组,最后才走toPath字符串解析; - 先用
get读当前值、再跑 updater 算新值——而set传的 updater 是恒返回目标值的箭头函数,这一步纯属陪跑; - 逐段遍历:每段过一遍
toKey归一化和isUnsafeToWriteProperty安全检查,非末段还要看下一段是不是下标(src/compat/_internal/isIndex.ts:非负整数或匹配^(?:0|[1-9]\d*)$的字符串)来决定建[]还是{}; - 每段写入都走
assignValue,里面还有"值相同就跳过赋值"的优化,避免触发多余的 setter。
上图是 es-toolkit 与 lodash 在其他函数上的性能对比——库的核心卖点是快。但set住在兼容层里,为了和 lodash 行为 100% 对齐,上面这套"兼容工序"一段都不能省。所以官方文档开头就挂了警告:路径在编译期已知时,直接用直接赋值或解构赋值,比 set 更快。
set 和直接赋值怎么选:场景对照表
最后给一张决策表。判断逻辑一句话:路径定死就别用 set,路径动态且要自动建结构,set 才是正解。
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 路径编译期固定 | obj.a.b.c = value | 零开销,可读性最好 |
| 需要不可变更新 | { ...obj, a: { ...obj.a, b: value } } | 触发引用变化,状态管理友好 |
| 只读不写 | obj?.a?.b?.c或get | 可选链是原生方案 |
| 路径动态(配置/用户输入/运行时拼接) | es-toolkit/compat的set | 直接赋值做不到,且需自动建结构 |
| lodash 存量代码迁移期 | set/setWith | 与_.set/_.setWith行为逐行对齐 |
| 要自定义中间容器(Map、类实例等) | setWith+ customizer | 默认的"下标建数组、其余建对象"不够用时 |
✅ es-toolkit compat set 的定位一句话:它解决的是兼容性与动态路径问题,不是性能问题。迁移存量代码用它替换_.set很安全;新代码则优先用现代语法表达,库的官方文档也是同样的建议。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考