JavaScript 深度合并函数 deepMerge 实现指南(附完整测试用例)

在日常 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 函数,兼顾了灵活性和安全性,通过可配置的有效值判断和类型检查,适配不同开发场景,同时严格遵循"不删除目标属性"的核心原则,支持任意深度嵌套。

代码可直接复制到项目中使用,测试用例覆盖了大部分高频场景,若需扩展(如循环引用处理、数组合并规则自定义),可基于现有逻辑轻松修改。

如果觉得有用,欢迎收藏、转发,也可以在评论区留言讨论你遇到的对象合并问题~

相关推荐
kyriewen7 小时前
Copilot下个月按Token收钱,我算了一笔账:重度用户一年要多花3000块
前端·javascript·openai
念恒123067 小时前
Python(for循环进阶)
开发语言·python
AI玫瑰助手7 小时前
Python运算符:算术运算符(加减乘除取模幂)详解
开发语言·python
xiaoye-duck7 小时前
Qt 信号与槽深度解析:connect 用法、自定义信号槽与 Lambda 实战
开发语言·qt
w_t_y_y7 小时前
VUE3(二)VUE2和VUE3区别
前端·javascript·vue.js
阿明在折腾7 小时前
在浏览器里实现 PDF 合并与拆分:pdf-lib 实战指南
前端·javascript
lsx2024067 小时前
C AI 编程助手:助力开发者高效编程
开发语言
沐知全栈开发7 小时前
Eclipse 编译项目指南
开发语言
无限进步_7 小时前
C++11概览与统一初始化
开发语言·c++