news 2026/9/16 1:39:54

es-toolkit 兼容层 updateWith 详解:用 customizer 精确控制嵌套对象路径的创建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
es-toolkit 兼容层 updateWith 详解:用 customizer 精确控制嵌套对象路径的创建

es-toolkit 兼容层 updateWith 详解:用 customizer 精确控制嵌套对象路径的创建

【免费下载链接】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

导读

本文以 es-toolkit 兼容层(compat)中面向 lodash 兼容的updateWith函数为核心,讲解如何在一个深层嵌套对象中,通过 updater 函数更新指定路径的值,并利用 customizer 回调控制路径缺失时中间对象(数组或普通对象)的创建形态。读完本文,你将掌握updateWith的完整签名、字符串/数组两种路径写法、customizer 的三个入参与返回约定,以及其底层实现如何协同gettoPathassignValueisIndex等内部模块完成路径解析与安全写入。

函数概览

updateWith是 es-toolkit 兼容层中update的增强版本(官方参考文档)。它与update的唯一区别在于:当路径中的某一段不存在时,update总是按「数字下标创建数组、其余创建普通对象」的默认规则补齐结构,而updateWith允许你通过 customizer 自定义这一段中间容器究竟应该是什么。

const updated = updateWith(obj, path, updater, customizer);

与 update / setWith 的关系

在 src/compat/object/update.ts 中可以看到,update实际上是updateWith的特例:

export function update(obj: object, path: PropertyPath, updater: (value: any) => any): any { return updateWith(obj, path, updater, () => undefined); }

也就是说,update等价于「customizer 恒返回undefined」的updateWith,此时完全走默认的中间对象创建逻辑。而setWith则是用固定值替换 updater 的同类函数(见 setWith 文档),内部同样基于updateWith的路径遍历机制实现。

基本用法:更新已存在路径上的值

当路径已存在时,updateWith的行为与update完全一致:取出旧值传给 updater,用返回值替换旧值。

import { updateWith } from 'es-toolkit/compat'; // 基本行为(与 update 相同) const object = { a: [{ b: { c: 3 } }] }; updateWith(object, 'a[0].b.c', n => n * n); // => { a: [{ b: { c: 9 } }] } // 使用数组形式的路径 updateWith(object, ['a', 0, 'b', 'c'], n => n + 10); // => { a: [{ b: { c: 13 } }] }

路径的两种写法

  • 字符串路径:如'a[0].b.c',支持点号与括号下标混合语法;空括号'a[]'会被解析为字面键''(测试 updateWith.spec.ts 验证了这一点),转义写法如'a[-1.23]["[\"b\"]"].c'也能正确解析。
  • 数组路径:如['a', 0, 'b', 'c'],元素可以是字符串、数字或 symbol。数组路径不会被强制转成字符串(['a', 'b', 'c']不会匹配'a,b,c'键),同时也不会忽略其中包含点号或空字符串的段(见 src/compat/object/updateWith.spec.ts 中「not coerce array paths to strings」「handle empty paths」两组用例)。

核心能力:用 customizer 控制中间对象的创建

当路径不存在时,updateWith需要沿途创建中间容器。默认规则是:若下一段路径是数组下标则创建数组,否则创建普通对象。而 customizer 可以改写这一规则:

import { updateWith } from 'es-toolkit/compat'; const object = {}; // 使用 Object 构造函数作为 customizer(强制创建普通对象而非数组) updateWith(object, '[0][1]', () => 'a', Object); // => { '0': { '1': 'a' } } // (默认行为会是 { '0': ['a'] })

customizer 接收(value, key, object)三个参数,其中value是当前路径段的旧值(通常为undefined),key是要创建的键,object是当前所在的父对象。它返回undefined时回退到默认行为,返回其他任意值则作为新建的中间容器:

import { updateWith } from 'es-toolkit/compat'; const customizer = (value: any, key: string, object: any) => { // 对数字键创建普通对象而不是数组 if (!isNaN(Number(key))) { return {}; } }; const result = {}; updateWith(result, '[0][1]', () => 'value', customizer); // => { '0': { '1': 'value' } }

路径已存在时 customizer 不会被调用

customizer 只负责「创建缺失的中间对象」,因此当整条路径都已经存在时,它不会被触发:

import { updateWith } from 'es-toolkit/compat'; const object = { a: { b: 1 } }; updateWith( object, 'a.b', n => n * 2, () => { console.log('Not called'); // 不会被调用 return {}; } ); // => { a: { b: 2 } }

测试 src/compat/object/updateWith.spec.ts 也专门验证了「customizer 修改原对象时,最后一级仍应被正确更新」这一边界行为。

源码级原理:updateWith 的执行链路

updateWith的实现位于 src/compat/object/updateWith.ts,核心流程分为四步:

1. 空值保护

if (obj == null && !isObject(obj)) { return obj; }

当传入nullundefined时直接原样返回,不会抛错(对应测试 updateWith.spec.ts 中的 nullish 用例)。

2. 路径解析

let resolvedPath: PropertyKey[]; if (isKey(path, obj)) { resolvedPath = [path]; } else if (Array.isArray(path)) { resolvedPath = path; } else { resolvedPath = toPath(path); }
  • 若路径本身是对象的已有键(isKey判定),直接作为单段路径;
  • 数组路径原样保留,逐段使用;
  • 字符串路径交给toPath(src/compat/util/toPath.ts)解析成段数组,支持点号与括号语法。

3. 计算新值

const updateValue = updater(get(obj, resolvedPath));

先通过get读取路径当前的旧值,交给 updater 计算出新值,再开始逐段写入。

4. 逐段写入与中间对象决策

for (let i = 0; i < resolvedPath.length && current != null; i++) { const key = toKey(resolvedPath[i]); if (isUnsafeToWriteProperty(key)) { return obj; } let newValue: unknown; if (i === resolvedPath.length - 1) { newValue = updateValue; } else { const objValue = current[key]; const customizerResult = customizer?.(objValue, key as string, obj); newValue = customizerResult !== undefined ? customizerResult : isObject(objValue) ? objValue : isIndex(resolvedPath[i + 1]) ? [] : {}; } assignValue(current, key, newValue); current = current[key]; }

这段循环揭示了三个关键设计:

  • 中间对象的决策优先级:customizer 返回值 > 已存在的对象(保留原引用)> 下一段是数组下标则建数组 > 否则建普通对象。其中「下一段是否为数组下标」由 src/compat/_internal/isIndex.ts 判定——它只把无符号十进制整数(0[1-9]\d*)视为下标,因此'1a'这类以数字开头但非下标的键仍会创建普通对象(测试 updateWith.spec.ts 专门覆盖了这一点)。
  • 原型链污染防护:每段写入前都会调用isUnsafeToWriteProperty(src/_internal/isUnsafeToWriteProperty.ts)检查__proto__constructorprototype等危险键,一旦命中立即中止整个更新并返回原对象。测试 updateWith.spec.ts 验证了'a[0][__proto__].b''constructor.prototype.polluted'等路径都不会污染全局Object.prototype
  • 惰性写入:最终赋值通过 src/compat/_internal/assignValue.ts 完成,它会在新旧值相同时跳过赋值(避免触发不必要的 setter),这对NaN、对象引用等场景同样适用(测试 updateWith.spec.ts)。

参数与返回值

参数类型说明
objT(object)要修改的目标对象。传入null/undefined时原样返回,不抛错
pathPropertyKey \| readonly PropertyKey[]要更新的属性路径,可用字符串(点号/括号语法)或数组表示
updater(oldValue: any) => any接收旧值、返回新值的函数
customizer(value: any, key: string, object: T) => any,可选决定缺失路径段要创建什么中间容器;返回undefined时走默认逻辑(下标建数组、其余建对象)

返回值T——修改后的原对象(原地修改,与update一致,测试中断言actualobject是同一引用)。

实践建议与注意事项

官方文档在页面开头明确提示:updateWith因复杂的路径解析与 customizer 处理而运行较慢,在性能敏感的代码中应优先使用现代的直接属性赋值或可选链(optional chaining)写法;updateWith更适合需要动态路径、且必须控制中间容器形态的 lodash 迁移场景。

使用时的几个要点:

  1. 路径尽量用数组形式:可避免字符串解析的歧义,也能正确处理包含点号、空串的键;
  2. customizer 记得在不需要定制时返回undefined:返回其他值会强制替换已存在的对象,可能破坏原有引用结构;
  3. 切勿传入__proto__/constructor/prototype相关路径:虽然实现内部会拦截并中止,但这类写法本身就是反模式;
  4. updateWith会原地修改对象:若不想改动原对象,请先自行浅拷贝/深拷贝。

相关函数与延伸阅读

  • update 参考文档:updateupdateWith的特例(customizer 恒为undefined);
  • setWith 参考文档:以固定值代替 updater,同样支持 customizer 控制中间对象形态;
  • updateWith 实现源码:核心遍历与写入逻辑;
  • updateWith 测试用例:覆盖路径解析、原型链污染防护、符号键、边界路径等 19 组场景;
  • set 相关实现 与 get 实现:updateWith读取旧值依赖get,写入依赖assignValue,三者共享同一套路径模型。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:38:47

DIODE数据集全解析:室内外稠密深度与法线标注

做单目深度估计和表面法线估计的&#xff0c;应该都遇到过一个问题&#xff1a;公开数据集要么只给深度&#xff0c;要么只在室内&#xff0c;要么标注稀疏到没法用。今天要聊的DIODE&#xff0c;是一个“全新RGB-D及平面法线数据集”&#xff0c;把室内、室外、稠密深度、平面…

作者头像 李华
网站建设 2026/9/16 1:38:28

Burp Suite实战:抓包、改包、重放、爆破四大核心模块详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:38:24

CentOS停服后迁移实战:Rocky Linux入门到运维高手

CentOS 7正式停服那天&#xff0c;我服务器上还有几十台生产机器没迁移完。算下来从接到通知到全部切换&#xff0c;前后折腾了三个多月&#xff0c;踩过的坑比走过的路还多。这期间我最终选定的替代方案就是Rocky Linux&#xff0c;也是今天这篇内容的主角。这篇内容不是那种泛…

作者头像 李华
网站建设 2026/9/16 1:35:40

Matlab中用粒子群算法优化XGBoost超参数实战

简介&#xff1a;本资源是一套基于Matlab实现的PSO-XGBoost混合智能算法分类预测完整方案&#xff0c;面向计算机、电子信息工程及数学等专业的本科生与研究生&#xff0c;适用于课程设计、期末大作业及毕业设计等实践场景&#xff0c;解决传统XGBoost超参数调优依赖经验、泛化…

作者头像 李华
网站建设 2026/9/16 1:34:44

Ubuntu安装Docker与Docker Compose:官方apt源配置与排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:34:17

Linkly AI:智能链接管理与UTM追踪工具解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华