es-toolkit 兼容层 set 函数源码详解与使用指南
es-toolkit 兼容层 set 函数源码详解与使用指南【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit路径只有在运行期才能拼出来、对象中间层级还可能缺失时往深层嵌套结构里写值怎么办直接赋值会一头撞上undefined。es-toolkit 的 compat 兼容层里的set就是为这种场景准备的。这是一篇源码级解析读完你会清楚_.set在迁移项目里到底保留了哪些行为、边界在哪以及何时该放弃它改用原生写法。30 秒上手跑通一次 es-toolkit/compat 的 setset的签名是set(object, path, value)它把value写到path指向的位置缺的层级自动补上。从兼容入口导入即可import { set } from es-toolkit/compat; const config {}; set(config, server.timeout, 5000); console.log(config); // { server: { timeout: 5000 } }config从空对象变成两层嵌套结构中间那层是set现场创建的。官方文档见 docs/compat/reference/object/set.md。set 用法全景四类写入场景覆盖路径上的已有值目标位置已经有值时新值直接覆盖不产生副本const user { profile: { city: hangzhou } }; set(user, profile.city, shanghai); // user.profile.city shanghaiset 自动创建缺失的嵌套结构这是它最核心的能力路径走到哪容器就建到哪const state {}; set(state, order.items.price, 99); // state { order: { items: { price: 99 } } }写入数组下标数字路径段按数组下标处理字符串2和数字2效果一致const row [1, 2, 3]; set(row, 2, 0); // row [1, 2, 0]对象与数组混合嵌套括号语法里下标和属性名可以任意交替容器类型跟着下一段自动切换const data {}; set(data, users[0].tags[0], vip); // data { users: [{ tags: [vip] }] }users下一段是下标所以建了数组tags下一段也是下标所以同样建数组——规则在后面源码部分细讲。set 行为契约用之前必须知道的事原地修改返回的就是原引用。测试用例直接断言expect(actual).toBe(object)见 src/compat/object/set.spec.ts#L92-L100。想拿到更新后的副本得自己先拷贝。null/undefined输入不抛错原样返回set(null, a.b, 1)得到null。已有值会被无条件覆盖没有合并语义。下一段是合法下标就创建数组否则创建对象这条规则决定了混合结构里每个缺口的类型。路径本身可以是数字或 symbolset(obj, 123, x)等价于写obj[123]。⚠️ 官方文档开头就挂着性能警告set内部委托给updateWith路径解析和对象创建逻辑较复杂速度明显慢于直接赋值或解构赋值。路径动态时用它没问题路径写死时请改用obj.a.b v这类原生写法。源码深拆set 的三步执行流程看 src/compat/object/set.ts#L89-L96 你会发现set本体只有 8 行export function setT extends object(obj: T, path: PropertyPath, value: any): T { return updateWith(obj, path, () value, () undefined); }真正的流水线在updateWithsrc/compat/object/updateWith.ts拆成三步看最清楚。第一步toPath 把路径拆成键的数组updateWith开头先做空值守卫obj null直接返回然后做路径归一化let resolvedPath: PropertyKey[]; if (isKey(path, obj)) { resolvedPath [path]; } else if (Array.isArray(path)) { resolvedPath path; } else { resolvedPath toPath(path); }片段源自 updateWith 路径解析字符串路径交给 src/compat/util/toPath.ts 解析相当于把省市区门牌号拆成一串地址段规则如下a.b.c→[a, b, c]a[b][c]同样结果引号内的点不拆分a[b.c].d→[a, b.c, d]括号里符合/^-?\d(?:\.\d)?$/的数字整体保留[-1.23]是一整段前导点产生空段.a.b→[, a, b]连续点a..b→[a, , b]空字符串路径 →[]第二步中间节点建数组还是对象路径解析完后先通过内部get读出当前值、交给 updaterset传的是恒定返回value的闭包然后逐段遍历。每段的核心决策在 中间节点创建逻辑if (i resolvedPath.length - 1) { newValue updateValue; } else { const objValue current[key]; newValue customizerResult ! undefined ? customizerResult : isObject(objValue) ? objValue : isIndex(resolvedPath[i 1]) ? [] : {}; }优先级从上到下customizer 结果 复用已有对象 看下一段是否下标。下标判定在 src/compat/_internal/isIndex.ts只认非负整数或匹配/^(?:0|[1-9]\d*)$/的字符串——所以1a不会触发数组创建。第三步逐段赋值并做安全保护每段先经toKey归一化再过一道isUnsafeToWriteProperty检查命中如__proto__就立刻返回原对象、放弃写入updateWith 安全检查。最后一跳交给 src/compat/_internal/assignValue.tsexport const assignValue (object: any, key: PropertyKey, value: any): void { const objValue object[key]; if (!(Object.hasOwn(object, key) eq(objValue, value)) || (value undefined !(key in object))) { object[key] value; } };注意那个eq短路目标位置已有相同值时跳过赋值避免无谓触发 setter——测试里用带 setter 的 getter 属性验证了这一点set 跳过相同值赋值。 set 细节与坑稀疏数组、原型保护等反直觉行为以下行为全部有测试用例背书迁移前值得过一遍跳过下标产生稀疏数组。set(list, items[2], x)而items不存在时items[1]是空位而非undefined属性set(object, a[1].b.c, v)的结果里object.a同样存在空洞测试用例。原型污染键被静默拦截。路径里出现__proto__这类不安全键时函数不做任何写入原对象原样返回由 src/_internal/isUnsafeToWriteProperty.ts 兜底。基本类型中间节点会被碾过。set({ a: }, a.b, 2)得到{ a: { b: 2 } }原来那个空字符串直接没了。数组路径不会被 join 成字符串。set(obj, [a, b, c], v)写的是obj.a.b.c而不是键名为a,b,c的属性测试用例。反过来字符串a.b和数组[a.b]都会被当作字面键名处理。空括号不忽略set(obj, a[], v)得到{ a: { : v } }数字前缀的键不建数组[1a, 2b, 3c]全程是普通对象测试用例-0与0作为键时符号各自保留。函数家族setWith 如何定制中间节点get 如何读setWith比set多一个customizer(nsValue, key, nsObject)专门接管中间节点建什么const obj {}; setWith(obj, [0][1], value, (nsValue, key) typeof key string /^\d$/.test(key) ? {} : undefined );customizer 返回值非undefined就直接用作该段容器这里强制建对象于是{ 0: [undefined, value] }变成{ 0: { 1: value } }返回undefined则回落默认规则。对照 src/compat/object/setWith.ts#L86-L101setWith只是把 customizer 透传给updateWith——也就是说set就是customizer 恒返回undefined的setWith特例。读取侧的 get 支持完全相同的路径语法多一个默认值参数get(obj, a.b.c, fallback)在路径断裂或obj为null时返回默认值同样不抛错。⚖️ 选型建议何时用 set何时直接原生赋值先放一组对照es-toolkit 相对 lodash 的整体性能背景可见仓库的 performance.png判断清单可以直接照着走用set路径是运行期拼接的中间层级可能缺失且需要自动创建从 lodash 迁移、需要_.set等价行为含括号/引号等复杂路径解析写的是数组下标等动态位置。用直接赋值obj.a.b v路径在编译期确定、层级已知存在。用展开/解构需要不可变更新set的原地修改语义帮不上忙。用可选链obj?.a?.b读场景别为了读个值拉起整条get→ 路径解析流水线。set的价值定位就一句话为动态路径和 lodash 行为兼容而存在性能从来不是它的卖点。迁移存量代码时它是安全网新代码里它应该是备选而非首选。【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考