> 在日常的 JavaScript 开发中,你是否曾遇到过需要将两个复杂的配置对象合并为一个的情况?或者,当你试图使用 Object.assign() 或展开运算符 (…) 来合并带有深层嵌套的对象时,发现内部的嵌套属性被直接覆盖而不是合并,从而导致数据丢失?
这是一个非常经典的问题。JavaScript 的标准对象合并方法通常是浅合并,这意味着它们只处理对象的第一层属性。而当我们需要处理深层嵌套结构时,我们需要更强大的工具。
在本文中,我们将深入探讨 Lodash 库中一个非常强大且常用的方法—— _.merge()。我们将通过丰富的代码示例,分析它的工作原理,并将其与其他合并方式进行对比,帮助你掌握在处理复杂对象合并时的最佳实践。无论你是正在构建复杂的配置系统,还是需要处理来自不同 API 的响应数据,这篇文章都将为你提供实用的见解。
什么是 Lodash 的 _.merge() 方法?
简单来说,_.merge() 是 Lodash 提供的一个用于递归合并对象的方法。与浅合并不同,它会递归地遍历对象的所有层级,将源对象中的属性合并到目标对象中。
#### 核心特性
- 递归合并:这是它最显著的特点。当遇到相同的键且值都是对象时,它会继续向下合并这两个对象,而不是直接用后者覆盖前者。
- 从左到右:合并顺序是从左向右进行的,后面的对象属性会覆盖前面对象中相同的属性值。
- 修改原始对象:请注意,该方法会直接修改第一个参数(目标对象)。如果你希望保留原始对象,需要在使用前创建一个副本。
- 数组处理:对于数组,它默认会通过追加元素的方式进行合并(如果对应位置的值是对象,也会尝试合并),这在某些配置合并场景中非常有用。
#### 语法结构
_.merge(object, [sources])
#### 参数说明
-
object:这是目标对象。合并的结果将放入这个对象中,并且该对象会被直接修改。 -
[sources]:这是源对象(可以有一个或多个)。它们的属性将被合并到目标对象中。
#### 返回值
该方法返回合并后的对象(实际上也就是第一个参数 object 的引用)。
基础示例:初识 _.merge()
让我们从最简单的例子开始,看看 _.merge() 是如何处理基本属性和重复键的。
在这个例子中,我们准备了一个包含 INLINECODE4852be09、INLINECODE29b38ede 和 python 属性的多个对象,通过不同的组合来观察合并结果。
// 引入 lodash 库
const _ = require("lodash");
// 示例 1:合并具有不同键的对象
// 这里我们将三个完全不冲突的对象合并成一个
console.log(
_.merge({ cpp: "12" }, { java: "23" }, { python: "35" })
);
// 输出: { cpp: ‘12‘, java: ‘23‘, python: ‘35‘ }
// 示例 2:处理键名冲突的情况
// 当多个对象中存在相同的键(如 ‘cpp‘)时,会发生什么?
// _.merge 会遵循"从左到右"的原则,最右侧的值会"覆盖"左侧的值。
console.log(
_.merge({ cpp: "12" }, { cpp: "23" }, { java: "23" }, { python: "35" })
);
// 输出: { cpp: ‘23‘, java: ‘23‘, python: ‘35‘ }
// 注意:‘cpp‘ 的最终值是 ‘23‘,取自最后一个包含该键的对象。
// 示例 3:完全相同的值
// 即使多个对象的值相同,合并后的结果也是唯一的键值对
console.log(
_.merge({ cpp: "12" }, { cpp: "12" }, { java: "23" }, { python: "35" })
);
// 输出: { cpp: ‘12‘, java: ‘23‘, python: ‘35‘ }
#### 代码解析
通过上述代码,我们可以观察到 _.merge() 的基本行为:它就像一个按顺序执行的覆盖过程。想象一下,你在刷墙,你先用红色的漆刷了一遍,然后又在同一面墙上用蓝色的漆刷了一遍。最终看到的颜色是蓝色,因为它是最后一次操作。在对象合并中,最右侧的对象属性拥有"最终决定权"。
进阶实战:递归合并嵌套对象
INLINECODEd7f18395 真正的威力在于处理嵌套结构。让我们看看它是如何与简单的展开运算符 (INLINECODE354eb81b) 或 Object.assign() 区分开来的。
假设我们有两个对象,INLINECODE46c92af5 和 INLINECODEf3964d0d。它们都有一个名为 amit 的属性,该属性的值是一个对象数组。我们希望合并这两个对象,并保留来自两个源的信息。
const _ = require("lodash");
// 目标对象:包含 ‘amit‘ 数组,数组内有对象
let object = {
‘amit‘: [{ ‘susanta‘: 20 }, { ‘durgam‘: 40 }]
};
// 源对象:同样包含 ‘amit‘ 数组,但对象属性不同
let other = {
‘amit‘: [{ ‘chinmoy‘: 30 }, { ‘kripamoy‘: 50 }]
};
// 使用 _.merge() 方法
// 注意:这里它将递归进入数组内部,合并数组下标相同的对象
let mergedResult = _.merge(object, other);
console.log(mergedResult);
/*
输出结果:
{
amit: [
{ susanta: 20, chinmoy: 30 }, // 注意:这里合并了下标 0 的对象
{ durgam: 40, kripamoy: 50 } // 这里合并了下标 1 的对象
]
}
*/
#### 深度解析
这是一个非常关键的例子。如果我们要使用原生的 Object.assign({}, object, other),结果会截然不同:
// 使用原生 Object.assign 对比
let assignResult = Object.assign({}, object, other);
console.log(assignResult);
/*
输出结果:
{
amit: [ { chinmoy: 30 }, { kripamoy: 50 } ]
}
注意:整个 ‘amit‘ 数组被 ‘other‘ 中的数组完全覆盖了,susanta 和 durgam 信息丢失!
*/
为什么会有这样的区别?
INLINECODE5e0ff74b 检测到 INLINECODE04cdd6cc 是一个数组(或对象),于是它"深挖"进去,逐个元素进行合并。而在我们的例子中,数组的元素也是对象。对于下标 0 的元素,INLINECODEe04946c0 将 INLINECODEf2118826 和 { ‘chinmoy‘: 30 } 合并成了一个新的对象。这就是我们所说的递归合并。
2026视角:现代工程化与配置管理最佳实践
在 2026 年的现代前端开发中,应用配置的复杂性远超以往。我们不仅要处理本地状态,还要面对微前端架构、边缘计算配置以及 AI Agent 的动态 Prompt 模板。_.merge() 在这方面依然是不可或缺的利器。
#### 实际应用场景:分层配置系统
在大型企业级应用中,我们通常遵循“配置叠加”的原则。让我们看一个更贴近 2026 年技术栈的例子:一个带有 AI 特性开关和环境参数的配置系统。
const _ = require("lodash");
// 1. 基础配置:定义应用的默认形态
const defaultConfig = {
appName: "NextGenApp",
version: "2.0.0",
features: {
// AI 相关功能的默认关闭状态
aiAssistant: { enabled: false, model: "gpt-4" },
// UI 默认设置
darkMode: false
},
api: {
timeout: 3000,
retries: 3
}
};
// 2. 环境配置:针对部署环境的特定覆盖(例如 Serverless 或 Edge 环境)
const envConfig = {
env: "production-edge",
api: {
// 在边缘环境中,我们通常需要更短的超时时间
timeout: 1000,
// 边缘节点特有的 headers
headers: { "X-Edge-Location": "asia-south-1" }
},
features: {
// 生产环境默认开启 AI 助手
aiAssistant: { enabled: true }
}
};
// 3. 用户租户配置:来自数据库的个性化设置
const tenantConfig = {
features: {
// 用户明确关闭了深色模式(虽然 default 中是 false,这里演示显式声明)
darkMode: false,
// 用户升级到了更高级的 AI 模型
aiAssistant: { model: "gpt-4-turbo-2026" }
},
// 自定义的域名或 API 端点
api: {
endpoint: "https://api.tenant-custom.com"
}
};
// 执行合并
// 这里的顺序至关重要:默认 -> 环境 -> 租户
// 优先级:租户 > 环境 > 默认
const finalConfig = _.merge({}, defaultConfig, envConfig, tenantConfig);
console.log(JSON.stringify(finalConfig, null, 2));
/*
最终输出结果分析:
{
"appName": "NextGenApp", // 来自 defaultConfig
"version": "2.0.0", // 来自 defaultConfig
"env": "production-edge", // 来自 envConfig
"features": {
"aiAssistant": {
"enabled": true, // 注意:来自 envConfig (覆盖了 default 的 false)
"model": "gpt-4-turbo-2026" // 注意:来自 tenantConfig (覆盖了 envConfig 的 model)
},
"darkMode": false // 来自 defaultConfig 或 tenantConfig
},
"api": {
"timeout": 1000, // 来自 envConfig (覆盖了 default)
"retries": 3, // 来自 defaultConfig (env 和 tenant 都没定义,所以保留了)
"headers": { ... }, // 来自 envConfig
"endpoint": "..." // 来自 tenantConfig
}
}
*/
关键洞察:在这个例子中,我们注意到 INLINECODEf282e194 如何巧妙地处理了 INLINECODE9813760c 对象。INLINECODE2687b02f 覆盖了 INLINECODE42542099,但保留了 INLINECODE6d7a60dc;同时 INLINECODEacfcd82b 增加了新的 INLINECODEea82530e,却没有干扰 INLINECODE423f9213 中设置的 timeout。这种非破坏性合并是构建高可配置系统的核心。
常见陷阱与最佳实践
虽然 _.merge() 很强大,但在使用时有几个坑需要你特别注意。
#### 1. 警惕:数组合并的“按索引合并”行为
这是开发者最容易踩的坑。当 _.merge() 遇到数组时,它会像处理对象一样,按索引合并数组中的元素。这在处理列表数据时通常不是我们想要的结果。
const object = {
‘users‘: [{ ‘id‘: 1, ‘name‘: ‘A‘ }, { ‘id‘: 2, ‘name‘: ‘B‘ }]
};
const other = {
‘users‘: [{ ‘age‘: 20 }, { ‘age‘: 25 }]
};
const result = _.merge(object, other);
/*
结果:
{
users: [
{ id: 1, name: ‘A‘, age: 20 }, // 索引 0 合并了
{ id: 2, name: ‘B‘, age: 25 } // 索引 1 合并了
]
}
*/
// 但是,如果第二个数组长度更长,或者你本意是想"替换"整个数组而不是"拼接"对象:
const other2 = {
‘users‘: [{ ‘id‘: 3, ‘name‘: ‘C‘ }] // 只有一个元素
};
const result2 = _.merge(object, other2);
// 结果: { users: [ { id: 3, name: ‘C‘ }, { id: 2, name: ‘B‘ } ] }
// 第一个数组元素被覆盖了,但第二个元素保留了!这可能不是你想要的。
解决方案:如果你希望源对象中的数组直接替换目标对象中的数组,而不是合并元素,你需要使用自定义的合并函数。
#### 2. 突变问题:保持数据不可变性
正如我们前面提到的,_.merge() 会修改第一个参数。在 React 或 Vue 这种依赖状态不可变性或响应式追踪的现代框架中,这可能导致 UI 不更新或难以追踪的 Bug。
// ❌ 错误示范:originalData 被修改了,可能导致组件意外重渲染或状态污染
const originalData = { a: 1 };
_.merge(originalData, { b: 2 });
console.log(originalData); // { a: 1, b: 2 }
// ✅ 正确示范:始终传入空对象作为第一个参数
const originalData = { a: 1 };
const newData = _.merge({}, originalData, { b: 2 });
console.log(originalData); // { a: 1 }
console.log(newData); // { a: 1, b: 2 }
深度定制:自定义合并策略
在 2026 年,我们面对的数据结构更加复杂。假设你正在开发一个 Agentic AI 系统的配置面板。你可能需要特殊处理某些字段,例如对于 INLINECODE0a6d5f5c 字段,你希望执行数组去重合并,而不是简单的对象合并。我们可以利用 INLINECODE87f1f9c3 来实现这一点。
const _ = require("lodash");
function customizer(objValue, srcValue) {
// 我们针对特定的键 ‘allowedModels‘ 进行特殊处理
// 如果源值和目标值都是数组,我们执行并集去重操作,而不是按索引合并
if (_.isArray(objValue)) {
// 这里我们假设对于数组,我们希望是"去重并集"而不是"按索引合并"
// 这是一个常见的需求,特别是处理标签列表或模型列表时
return _.union(objValue, srcValue);
}
}
// 目标对象
const object = {
name: "Agent-007",
allowedModels: ["gpt-4", "claude-2"],
settings: { creativity: 0.7 }
};
// 源对象
const source = {
allowedModels: ["claude-2", "gemini-pro"], // 注意这里有重复的 claude-2
settings: { creativity: 0.9 }
};
// 使用自定义合并逻辑
const result = _.mergeWith({}, object, source, customizer);
console.log(result);
/*
输出:
{
name: ‘Agent-007‘,
allowedModels: [ ‘gpt-4‘, ‘claude-2‘, ‘gemini-pro‘ ],
settings: { creativity: 0.9 }
}
注意:allowedModels 变成了三个模型的去重数组,而不是 [ ‘gpt-4‘, ‘claude-2‘, ‘gemini-pro‘ ] 覆盖前两个。
而 settings 对象则继续执行了默认的递归合并。
*/
性能优化与 2026 替代方案
对于大多数应用场景,_.merge() 的性能是完全足够的。但是,如果你需要在高性能循环中处理大量数据,或者对象层级极深,请考虑以下建议:
- 避免不必要的深拷贝:如果你确定对象只有一层,使用 INLINECODE3bce6734 或展开运算符 INLINECODEe832e712 会更快。
- Tree-Shaking 的考量:在 2026 年,随着 Vite 和 Turbopack 的普及,bundle size 的大小至关重要。如果只是为了这一个方法引入整个 Lodash 库是不划算的。
* Lodash-es:使用 INLINECODEde05d4ab 并按需引入 INLINECODE9d91b1aa。
* 原生替代:对于非常简单的深拷贝,现代浏览器的 structuredClone() API 是一个高性能的原生替代方案,但它不提供“合并”功能,只能克隆。
* 轻量级库:可以考虑使用只包含核心功能的库,如 lodash.merge 的独立包。
总结
Lodash 的 _.merge() 方法是处理复杂 JavaScript 对象的神器。通过递归地合并属性,它解决了原生浅合并方法无法处理的深层嵌套问题。
在本文中,我们学习了:
- 基本用法:如何从左到右合并对象,以及后传入的属性如何覆盖前面的属性。
- 深层合并:通过嵌套对象的例子,看到了
_.merge如何逐层深入合并数据,而不仅仅是简单的引用替换。 - 实际场景:如何在配置管理(默认配置、环境配置、用户配置)中优雅地运用它。
- 陷阱规避:特别注意数组合并的特殊行为以及原地修改带来的副作用。
下次当你面临复杂的数据合并任务时,不妨试试 _.merge(),它可能会为你省去编写大量递归逻辑的时间。