es-toolkit set 深度指南:3 种路径语法写入嵌套对象,动态 key 一步到位
【免费下载链接】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
数据 key 从后端配置拼出来时,"往user.address.city写个值"这种需求,用obj?.a?.b = v一行都写不出——层数在编译期根本定不了。es-toolkit 兼容层的set就是为这类场景准备的:接收一个动态路径字符串,在任意深度写入值,缺什么节点就自动补什么节点。
快速上手:8 行代码跑通最小用例
import { set } from 'es-toolkit/compat'; const config = {}; set(config, 'user.profile.city', '上海'); console.log(config); // { user: { profile: { city: '上海' } } }config原本只是个空对象,set沿着'user.profile.city'逐层建好容器,把值写进最深处。实现上它只是 src/compat/object/set.ts 里 5 行的薄壳,真正的活都交给了updateWith:
export function set(obj, path, value) { return updateWith(obj, path, () => value, () => undefined); }高频场景
场景 1:路径来自运行期,写死赋值不现实
表单字段映射、远端下发的配置项,路径只能运行期才知道:
const field = request.body.target; // 例如 'theme.mode' set(state, field, 'dark');原理一句话:字符串路径先被toPath拆成段数组(点号、括号都是分隔符),再逐段走树。
场景 2:从零开始建结构,连"下标跳跃"都帮你兜住
const doc = {}; set(doc, 'items[1].price', 42); // { items: [undefined, { price: 42 }] }中间节点不存在时,set会看下一段是不是下标:是就建数组[],否则建对象{}(判断逻辑在 src/compat/_internal/isIndex.ts)。注意这里跳过下标 0,items[0]是空洞——这是稀疏数组,for...of遍历时会被跳过,别意外。
场景 3:键名本身带点号,用括号或数组路径绕过
set(obj, 'a["b.c"].d', 1); // 引号里的点不拆分 → obj.a['b.c'].d set(obj, ['a.b'], 2); // 数组元素按字面键 → obj['a.b']两条规则都指向同一个结论:字符串路径里"带引号或带括号的段"和"数组形式的单个元素"都当作字面键名,不做二次拆分。这在键名来自外部输入、无法预测时能救命。
场景 4:往数组里按位置改值
const arr = [1, 2, 3]; set(arr, 1, 4); console.log(arr); // [1, 4, 3]数字直接当下标用,不需要字符串包装;对象套数组、数组套对象这种混合嵌套同样支持,比如set({}, 'users[0].name', 'a')会得到{ users: [{ name: 'a' }] }。
⚠️ 容易踩坑的隐性规则
以下行为都有测试用例背书(见 src/compat/object/set.spec.ts),迁移 lodash 代码时建议逐条过一遍:
| 隐性规则 | 输入 → 输出 | 为什么 |
|---|---|---|
| 原地修改,返回同一个引用 | const r = set(o, 'y', 2)后r === o为true | updateWith全程在改原对象,最后原样返回,不做拷贝 |
| 数字下标建数组,其余建对象 | set({}, 'a[1].b', 1)→{ a: [undefined, { b: 1 }] } | 下一段通过isIndex判定(非负整数,或/^(?:0\|[1-9]\d*)$/字符串)才建[] |
| nullish 输入静默返回 | set(null, 'a.b', 1)返回null,不抛错 | 函数入口先判空直接 return |
| 危险键名静默中止 | set({}, 'a.__proto__.x', 1)不写任何东西 | 每段键名都过isUnsafeToWriteProperty,命中__proto__/constructor/prototype立即中止(见 src/_internal/isUnsafeToWriteProperty.ts) |
| 中间是基本类型会被整个覆盖 | set({ a: '' }, 'a.b', 2)→{ a: { b: 2 } } | 中间节点不是对象就无条件重建容器,原值直接丢弃 |
额外一个细节:写入前会用
eq比较目标位置的值,相同就跳过赋值,不会触发多余的 setter(测试里专门给带 setter 的属性做了断言)。
性能与替代方案
官方文档在 docs/compat/reference/object/set.md 开头就给了警告:set走完整路径解析 +get读旧值 + 逐段建容器 + 逐段赋值的流水线,速度较慢,优先用直接赋值或解构赋值。
| 需求 | 现代语法 | set |
|---|---|---|
| 路径编译期已知 | obj.a.b.c = 1(直接赋值,最快) | 能做但纯属杀鸡用牛刀 |
| 不可变更新 | { ...obj, a: { ...obj.a, b: 1 } } | 做不到(它本来就改原对象) |
| 路径运行期才知道 | 得手写递归写入 | ✅ 核心用途 |
| 中间节点缺失需自动创建 | 手写reduce或Object.assign链 | ✅ 内置 |
lodash_.set存量代码迁移 | 需重写 | ✅ 行为对齐 |
结论很直白:只要路径在编译期能写死,就该放弃set。它的价值全部押在"路径是动态的"这一条上,为此付出的代价是每次调用都多一整套解析和判空逻辑。
🛠️ 进阶延伸
默认"下标→数组、其余→对象"的建容器策略不够用时,换setWith——它多收一个 customizer 回调,返回undefined时回落到默认策略:
import { setWith } from 'es-toolkit/compat'; const obj = {}; setWith(obj, '[0][1]', 'v', () => []); // { '0': [undefined, 'v'] }反过来,set就是"customizer 恒返回undefined"的setWith特例(见 src/compat/object/setWith.ts)。读取侧配套的是get(obj, 'a.b.c', 'default'),同样支持路径语法且 null 安全。
决策清单
- ✅该用:路径是动态字符串(用户输入 / 配置下发 / lodash 迁移)
- ✅该用:需要"缺失节点自动创建"且不想手写递归
- ❌不该用:路径编译期已知,直接
obj.a.b = 1更快更省 - ❌不该用:需要不可变语义——
set永远原地修改并返回原引用 - ❌不该用:对
null/undefined期望有副作用——它静默返回原值,错误会被吞掉
一句话总结: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),仅供参考