
es-toolkit 兼容版 mergeWith 深度指南用自定义合并函数掌控对象深层合并【免费下载链接】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-toolkitmergeWith是 es-toolkit 为 lodash 兼容场景提供的对象深层合并函数它允许你传入一个自定义函数customizer来逐属性控制合并行为——从数值相加、数组拼接到按 key 应用不同规则都可以轻松实现。本指南以 docs/compat/reference/object/mergeWith.md 为核心结合 src/compat/object/mergeWith.ts 源码与其测试用例 mergeWith.spec.ts完整讲解 API 签名、六参数 customizer 的用法、默认合并逻辑以及循环引用与原型污染防护等底层原理。读完你可以在保持与 lodash 行为一致的前提下用最少的代码实现任意自定义合并策略。一、先认识两个 mergeWith兼容版与现代版在动手使用之前需要先区分 es-toolkit 仓库中存在的两个同名函数兼容版Lodash Compatibility位于es-toolkit/compat对应文档 docs/compat/reference/object/mergeWith.md源码为 src/compat/object/mergeWith.ts。它完整复刻 lodashmergeWith的语义支持多个 source 对象customizer 可接收 6 个参数含stack。现代版es-toolkit 原生位于es-toolkit/object对应文档 docs/reference/object/mergeWith.md源码为 src/object/mergeWith.ts。它更快、更轻量但 customizer 只接收 5 个参数不含stack且一次只能合并一个 source。仓库文档对兼容版给出了明确的性能提示由于需要复杂的类型检查、循环引用处理和特殊对象处理兼容版mergeWith相对较慢如果不需要 lodash 级兼容行为建议优先使用 es-toolkit 原生版本的mergeWith。本篇文章聚焦兼容版因为它的 customizer 参数最丰富、行为最贴近 lodash是迁移旧代码时的首选。二、API 概览签名、参数与返回值兼容版mergeWith的调用形式为const result mergeWith(target, ...sources, customizer);参数说明参数类型说明objectany目标对象合并结果会直接修改并返回它...sourcesany[]一个或多个源对象依次合并进目标对象customizerMergeWithCustomizer自定义合并函数格式为(objValue, srcValue, key, object, source, stack) any返回值any合并完成后的目标对象与传入的object是同一个引用原地修改。导入方式import { mergeWith } from es-toolkit/compat;三、customizer 核心语义返回 undefined 即回退默认逻辑mergeWith的精髓在于 customizer 与默认合并逻辑的协作契约如果 customizer 返回undefined则对该属性使用默认的深度合并逻辑如果返回其他任何值包括null则直接采用返回值作为该属性的合并结果。这一点在源码中有明确体现src/compat/object/mergeWith.tsconst merged merge(targetValue, sourceValue, key, target, source, stack); if (merged ! undefined) { target[key] merged; } else if (Array.isArray(sourceValue)) { target[key] mergeWithDeep(targetValue, sourceValue, merge, stack); } else if (/* 对象且可合并 */) { target[key] mergeWithDeep(targetValue, sourceValue, merge, stack); } else if (/* ... */) { target[key] sourceValue; }正是因为merged ! undefined才走 customizer 分支所以无法通过返回undefined来删除某个属性而返回null是可以生效的——测试用例 mergeWith.spec.ts 专门验证了这一点当 customizer 对targetValue null的属性返回null时源对象中的同名对象不会覆盖它。四、四种典型实战用法4.1 数字相加import { mergeWith } from es-toolkit/compat; const obj1 { a: 1, b: 2 }; const obj2 { b: 3, c: 4 }; const result mergeWith(obj1, obj2, (objValue, srcValue) { if (typeof objValue number typeof srcValue number) { return objValue srcValue; } }); // Result: { a: 1, b: 5, c: 4 }4.2 数组合并拼接而非按下标覆盖const arr1 { items: [1, 2] }; const arr2 { items: [3, 4] }; const merged mergeWith(arr1, arr2, (objValue, srcValue) { if (Array.isArray(objValue)) { return objValue.concat(srcValue); } }); // Result: { items: [1, 2, 3, 4] }注意默认的数组合并是按下标覆盖即[1,2]与[3,4]合并得到[3,4]要得到[1,2,3,4]就必须借助 customizer 的concat。4.3 按 key 定制规则字符串拼接customizer 的第三个参数key让你可以针对特定字段应用专属逻辑其他字段走默认合并const str1 { message: Hello }; const str2 { message: World }; const combined mergeWith(str1, str2, (objValue, srcValue, key) { if (key message typeof objValue string) { return objValue srcValue; } }); // Result: { message: Hello World }类似的按 key 分流思想也出现在原生版文档示例中对timeout取Math.max、对retries取Math.min其余字段默认合并见 docs/reference/object/mergeWith.md这种模式在配置合并场景中非常实用。4.4 多个源对象依次合并mergeWith支持任意数量的源对象它们会从左到右依次合并进目标对象const base { scores: [80] }; const quiz1 { scores: [90] }; const quiz2 { scores: [85] }; const final mergeWith(base, quiz1, quiz2, (objValue, srcValue) { if (Array.isArray(objValue)) { return objValue.concat(srcValue); } }); // Result: { scores: [80, 90, 85] }从源码看多源合并是在循环中逐个调用内部mergeWithDeep完成的src/compat/object/mergeWith.tslet result object; for (let i 0; i sources.length; i) { result mergeWithDeep(result, sources[i], merge, new Map()); }五、customizer 的完整参数六参数签名与 es-toolkit 原生版5 参数不同兼容版 customizer 接收6 个参数import { mergeWith } from es-toolkit/compat; const customizer (objValue, srcValue, key, object, source, stack) { console.log(Merging:, key, objValue, -, srcValue); // 仅对特定 key 定制 if (key specialField) { return ${objValue}_${srcValue}; } // 返回 undefined 走默认合并逻辑 return undefined; };参数含义objValue目标对象中当前属性的值srcValue源对象中当前属性的值key正在合并的属性名string \| symbolobject目标对象本身source源对象本身stack一个Map用于追踪已处理的对象以处理循环引用类型定义为MergeWithCustomizersrc/compat/object/mergeWith.tstype MergeWithCustomizer (objValue: any, srcValue: any, key: string, object: any, source: any, stack: any) any;关于stack的实证测试用例 mergeWith.spec.ts 通过last(arguments)取出 customizer 的最后一个参数并断言actual instanceof Map为true——这证实了stack确实是一个Map实例并且它会在mergeWithDeep中被递归传递stack.set(source, target)、stack.has(source)判断用于检测和克隆循环引用。六、默认合并逻辑customizer 不接管时发生什么当 customizer 返回undefined时底层mergeWithDeep会按以下顺序决策对应源码 src/compat/object/mergeWith.ts源值是数组对目标值做数组形态归一化后递归合并。如果目标值也是数组会先克隆目标数组连同其自定义属性通过Reflect.ownKeys复制如果是类数组对象只复制数字索引否则目标值直接变为[]。两边都是可合并对象当目标值与源值都是isObjectLike且至少一方是isPlainObject或isTypedArray时递归深度合并。目标值为空且源值是纯对象以{}为基底递归合并源值。目标值为空且源值是 TypedArray直接cloneDeep源值。兜底赋值targetValue undefined || sourceValue ! undefined时用源值覆盖目标值。对普通属性默认行为可概括为两点源码注释与原生版文档一致源值为数组/对象且目标值同为数组/对象时递归合并源值为undefined时不会覆盖目标对象中已定义的属性。验证用例mergeWith({}, { a: 1 }, noop)得到{ a: 1 }而mergeWith({ a: 1 }, { a: undefined }, noop)仍保留{ a: 1 }见 mergeWith.spec.ts。七、边界情况与特殊对象处理兼容版之所以慢是因为它要处理大量 lodash 才需要的边界场景。以下行为均有测试用例佐证mergeWith.spec.ts7.1 循环引用源对象自引用时不会死循环而是通过stack检测并克隆已处理对象const source: any { a: 1 }; source.circular source; const result mergeWith({ b: 2 }, source, noop); // result.a 1result.b 2result.circular 存在且可继续访问7.2 原型污染防护合并时会跳过__proto__这类不安全属性内部工具函数 src/_internal/isUnsafeProperty.ts 专门检测__proto__。测试确认源对象中的__proto__不会污染目标对象的原型链const result mergeWith( { a: 0, [__proto__]: { polluted: no } }, { a: 1, [__proto__]: { polluted: yes } }, noop ); // result 为 { a: 1, [__proto__]: { polluted: no } }7.3 特殊对象类型Arguments 对象源或目标中的arguments会被展开为普通对象再合并BufferNode.js源中的 Buffer 会被cloneDeep克隆结果toBeInstanceOf(Buffer)且与原对象不共享引用TypedArray源中的Uint8Array等会被克隆Symbol 属性通过getSymbols收集Symbol 键也会参与合并。7.4 空值与原始值目标不传有效 source 时返回原目标对象mergeWith(target, null, noop) target目标为null/undefined时等价于从空对象开始合并目标为原始值数字、字符串、布尔时会被包装为对象再合并如mergeWith(1, { a: 1 }, noop)等价于Object.assign(1, { a: 1 })。八、与 merge 的关系mergeWith 是底层实现理解mergeWith后你会发现es-toolkit 兼容版的merge无 customizer 的普通深合并正是mergeWith的特例——它把noop作为 customizer 传入src/compat/object/merge.tsexport function merge(object: any, ...sources: any[]): any { return mergeWith(object, ...sources, noop); }由于noop恒返回undefined所有属性都会落入默认合并逻辑。这也意味着你掌握的mergeWith行为完全可以迁移到merge的使用场景中反过来当默认合并无法满足需求拼接数组、累加数值时升级为mergeWith并补充 customizer 即可。九、快速上手与选型建议安装与引入npm install es-toolkit// 兼容版本文主角行为对齐 lodash import { mergeWith } from es-toolkit/compat; // 原生版更快更小单 source import { mergeWith } from es-toolkit/object;选型建议场景推荐版本从 lodash 迁移、需要多 source 与stack参数、追求行为完全一致es-toolkit/compat的mergeWith新项目、追求性能与包体积、单 source 即可满足es-toolkit/object的mergeWith性能取舍可直接查阅仓库基准数据 docs/performance.md 与 docs/compat/reference/object/mergeWith.md 中的警告说明两者对同一问题的权衡一目了然。十、总结mergeWith是 es-toolkit 兼容层中最灵活的合并工具它以customizer 返回undefined即回退默认深度合并为契约让你能对数组、数值、字符串乃至任意键名施加专属规则底层mergeWithDeep则通过stack循环引用追踪、isUnsafeProperty原型污染防护、Arguments/Buffer/TypedArray 特殊对象归一化等一系列处理保证了与 lodash 一致的行为边界。在需要多源合并、按 key 分流策略或拼接数组的场景中mergeWith是迁移与新写代码都值得优先考虑的选择。【免费下载链接】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),仅供参考