在日常 JavaScript 开发中,对象合并是高频需求------小到配置合并、接口返回数据整合,大到复杂状态管理,都离不开"深度合并"的支持。不同于 Object.assign() 或扩展运算符(...)的浅合并,深度合并需要递归处理嵌套对象,同时兼顾灵活性和安全性。
本文将实现一个满足多场景需求的 deepMerge 函数,支持自定义有效值判断、可配置类型检查,严格遵循"不删除目标属性"的核心原则,适配任意深度嵌套场景,同时附上完整测试用例,方便直接复用。
一、需求拆解:一个合格的深度合并函数该具备什么?
在动手实现前,我们先明确 deepMerge(target, source, options) 的核心需求,避免踩坑:
-
不删属性:无论 source 如何,绝不能删除 target 中已有的任何属性;
-
有效值可控:支持自定义"有效值"判断,默认排除 undefined 和 null;
-
类型检查可配置:可选择是否校验 source 和 target 对应属性的类型,默认开启严格校验;
-
类型精确区分:能精准区分 null、基本类型、数组、日期、正则、普通对象;
-
深度递归:支持任意层级嵌套对象(如五层及以上),普通对象递归合并,其他类型直接覆盖;
-
无外部依赖:所有辅助逻辑内置,直接调用即可。
二、完整实现:deepMerge 函数(带详细注释)
以下是满足所有需求的完整代码,每一步都添加了清晰注释,便于理解核心逻辑:
javascript
/**
* 深度合并 source 到 target 中(直接修改 target 并返回)
* @param {Object} target - 目标对象(会被直接修改)
* @param {*} source - 源对象
* @param {Object} [options={}] - 配置项
* @param {Function} [options.isValid] - 自定义有效值判断函数
* @param {boolean} [options.checkType=true] - 是否开启类型严格检查
* @returns {Object} 修改后的 target 对象
*/
function deepMerge(target, source, options = {}) {
// ====================== 内部辅助工具函数(全部内置,无外部依赖)======================
/**
* 获取值的精确类型(满足精确区分需求)
* @param {*} val - 任意值
* @returns {string} 小写类型字符串(如 'null'、'array'、'date'、'object')
*/
function getExactType(val) {
// 处理 null 特殊情况(Object.prototype.toString.call(null) 返回 '[object Null]')
if (val === null) return 'null';
// 利用 Object.prototype.toString 精准判断类型,避免 typeof 的局限性
const typeStr = Object.prototype.toString.call(val);
// 提取类型字符串(如从 '[object Array]' 提取 'array')并转为小写
return typeStr.slice(8, -1).toLowerCase();
}
/**
* 判断是否为普通对象({} / new Object()),排除数组、日期、正则等特殊对象
* @param {*} val - 任意值
* @returns {boolean}
*/
function isPlainObject(val) {
// 仅当精确类型为 'object' 时,才是普通对象
return getExactType(val) === 'object';
}
/**
* 判断值是否为有效值(支持自定义逻辑)
* @param {*} val - 任意值
* @returns {boolean}
*/
function isValueValid(val) {
const { isValid } = options;
// 1. 传入自定义有效函数 → 优先使用自定义逻辑
if (typeof isValid === 'function') {
return isValid(val);
}
// 2. 默认规则:非 undefined、非 null 即为有效
return val !== undefined && val !== null;
}
// ====================== 核心合并逻辑 ======================
// 前置检查:source 本身无效 → 直接返回 target,不做任何操作
if (!isValueValid(source)) {
return target;
}
// 仅处理对象类型的 source(非对象无法遍历属性,无需合并)
if (typeof source !== 'object' || source === null) {
return target;
}
// 遍历 source 的所有自身可枚举属性(跳过原型链属性)
for (const key in source) {
if (!source.hasOwnProperty(key)) continue;
const sourceVal = source[key];
const targetVal = target[key];
// 核心规则:无效值完全忽略,不做任何赋值/递归操作
if (!isValueValid(sourceVal)) continue;
// 分支1:target 无此属性 → 直接新增该属性(有效值已校验)
if (!target.hasOwnProperty(key)) {
target[key] = sourceVal;
continue;
}
// 分支2:target 已有此属性 → 执行合并/覆盖逻辑
const checkType = options.checkType !== false; // 默认开启类型检查
const sourceType = getExactType(sourceVal);
const targetType = getExactType(targetVal);
// 开启类型检查 && 类型不同 → 跳过该属性(不修改、不递归)
if (checkType && sourceType !== targetType) continue;
// 双方都是普通对象 → 递归调用 deepMerge,继续合并子属性
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
deepMerge(targetVal, sourceVal, options);
continue;
}
// 其他情况(数组、日期、正则、基本类型等)→ 直接覆盖 target 的值
target[key] = sourceVal;
}
// 返回修改后的目标对象(支持链式调用)
return target;
}
三、核心逻辑解析
很多同学实现深度合并时,容易出现"类型判断不精准""无效值处理遗漏""递归死循环"等问题,这里重点解析几个关键逻辑:
1. 精确类型判断:getExactType 函数
typeof 存在明显局限性(如 typeof null === 'object'、typeof [] === 'object'),因此我们使用 Object.prototype.toString.call(val) 来获取精确类型,再提取类型字符串,确保能区分以下所有类型:
undefined、null、string、number、boolean、bigint、symbol、function、array、date、regexp、object(普通对象)。
2. 普通对象判断:isPlainObject 函数
合并时,只有"普通对象"({} 或 new Object())需要递归合并,数组、日期、正则等特殊对象直接覆盖即可。这个函数通过判断精确类型是否为 'object',排除了其他特殊对象。
3. 有效值处理:isValueValid 函数
支持自定义有效值逻辑,比如排除空字符串、0、false、NaN 等,默认仅排除 undefined 和 null。同时,source 本身也会经过这个函数校验,若 source 无效,直接返回 target。
4. 类型检查与递归合并
当 target 已有该属性时,先判断是否开启类型检查:开启则类型不同跳过,关闭则直接合并;若双方都是普通对象,递归合并子属性,否则直接覆盖。
四、测试用例验证(覆盖所有核心场景)
以下测试用例覆盖了默认规则、自定义有效值、类型检查、深度嵌套、特殊类型(数组/日期/正则)等场景,复制到控制台即可运行验证:
测试用例 1:默认规则(排除 undefined 和 null)
javascript
const target1 = { a: 1, b: 2 };
const source1 = { a: undefined, b: null, c: 3 };
deepMerge(target1, source1);
console.log('测试用例1结果:', target1);
// 期望输出:{ a: 1, b: 2, c: 3 } ✅
测试用例 2:自定义有效值(排除空字符串、0、false)
javascript
const target2 = { a: 1, b: 'keep', c: true };
const source2 = { a: '', b: 0, c: false, d: 'new' };
const isValidCustom = (val) => {
if (val === undefined || val === null) return false;
if (val === '') return false;
if (val === 0) return false;
if (val === false) return false;
return true;
};
deepMerge(target2, source2, { isValid: isValidCustom });
console.log('测试用例2结果:', target2);
// 期望输出:{ a: 1, b: 'keep', c: true, d: 'new' } ✅
测试用例 3:关闭类型检查(允许跨类型覆盖)
javascript
const target3 = { a: 123 };
const source3 = { a: 'string' };
deepMerge(target3, source3, { checkType: false });
console.log('测试用例3结果:', target3);
// 期望输出:{ a: 'string' } ✅
测试用例 4:五层嵌套 + 排除 NaN
javascript
const target4 = {
l1: { l2: { l3: { l4: { l5: { num: 100, text: 'ok' } } } } }
};
const source4 = {
l1: { l2: { l3: { l4: { l5: { num: NaN, text: null, extra: 'new' } } } } }
};
const isValidNoNaN = (val) => {
if (val === undefined || val === null) return false;
if (typeof val === 'number' && isNaN(val)) return false;
return true;
};
deepMerge(target4, source4, { isValid: isValidNoNaN });
console.log('测试用例4结果:', JSON.stringify(target4, null, 2));
// 期望输出:l5: { num: 100, text: 'ok', extra: 'new' } ✅
测试用例 5:特殊类型合并(数组、日期、正则)
javascript
const target5 = {
arr: [1, 2],
date: new Date(2024, 0, 1),
reg: /test/g
};
const source5 = {
arr: [3, 4, 5],
date: new Date(2025, 11, 31),
reg: /hello/i
};
deepMerge(target5, source5);
console.log('测试用例5结果:', target5);
// 期望输出:arr: [3,4,5]、date: 2025-12-31、reg: /hello/i ✅
五、使用场景与注意事项
1. 适用场景
-
配置合并:如项目基础配置与环境配置合并;
-
接口数据整合:如后端返回的基础数据与前端本地补充数据合并;
-
状态管理:如 Vue/React 中,合并新旧状态(避免删除已有状态)。
2. 注意事项
-
直接修改 target:函数会直接修改传入的 target 对象,若需保留原 target,可传入 target 的浅拷贝(如 deepMerge({...target}, source));
-
函数类型处理:函数会被视为基本类型,直接覆盖(符合多数开发场景);
-
循环引用:若对象存在循环引用(如 a.b = a),会导致递归死循环,需根据实际场景添加循环引用判断(本文暂不涉及,可按需扩展)。
六、总结
本文实现的 deepMerge 函数,兼顾了灵活性和安全性,通过可配置的有效值判断和类型检查,适配不同开发场景,同时严格遵循"不删除目标属性"的核心原则,支持任意深度嵌套。
代码可直接复制到项目中使用,测试用例覆盖了大部分高频场景,若需扩展(如循环引用处理、数组合并规则自定义),可基于现有逻辑轻松修改。
如果觉得有用,欢迎收藏、转发,也可以在评论区留言讨论你遇到的对象合并问题~