Skip to content
概述
deepMerge 是对两个 JSON 值进行深度递归合并的工具函数,源自 LeetCode 题目 2755。它可视为 Object.assign 的递归增强版:当属性值仍然是对象或数组时,会继续向下合并,而不是简单覆盖。本节针对该实现的合并规则、代码结构与边界行为进行拆解。
基本概念
浅合并(shallow merge)只处理第一层属性,若属性值为引用类型,则直接覆盖整个引用,例如 Object.assign({ a: { x: 1 } }, { a: { y: 2 } }) 会丢弃 x。深度合并(deep merge)会递归进入嵌套的数组和对象,在保留原有结构的前提下,将新值融入进去。两者差异示意:
ts
// 浅合并
const s = Object.assign({ a: { x: 1 } }, { a: { y: 2 } })
// s.a -> { y: 2 },丢失 x
// 深度合并
const d = deepMerge({ a: { x: 1 } }, { a: { y: 2 } })
// d.a -> { x: 1, y: 2 },两个属性都保留工作原理
deepMerge 接收两个 JSONValue 类型的参数 obj1 和 obj2,返回合并后的新值。合并过程遵循以下 5 条规则,规则按优先级排列:
obj1为null→ 直接返回obj2null被视为无实际值,此时obj2全部覆盖。两个参数的类型不同 → 返回
obj2
例如一个是数组、另一个是对象,无法合并,新值直接取代旧值。两者均为数组 → 按索引合并
以obj1为基底(浅拷贝),依次处理obj2的每个下标:- 若
obj1对应位置存在且为对象,则递归合并; - 否则用
obj2对应位置的元素覆盖。obj2长度大于obj1时,多余元素直接追加到末尾。
- 若
两者均为普通对象 → 按 key 合并
以obj1为基底(浅拷贝),遍历obj2的所有自身属性:- 若 key 在
obj1中也存在,递归合并该 key 下的值; - 若 key 仅存在于
obj2,直接赋值。
- 若 key 在
其他情况(基本类型)→ 返回
obj2
数字、字符串、布尔值等情况,无合并可能,直接覆盖。
deepMerge 使用展开运算符进行浅拷贝({ ...obj1 } 或 [...obj1]),再逐个字段做深度处理,因此原始 obj1 和 obj2 都不会被修改。下面是完整实现:
ts
type JSONValue =
| null
| boolean
| number
| string
| JSONValue[]
| { [key: string]: JSONValue }
type JSONObject = { [key: string]: JSONValue }
function isObject(value: JSONValue): value is JSONObject {
return typeof value === 'object' && value !== null
}
const type = (obj: any) => Object.prototype.toString.call(obj).slice(8, -1)
function deepMerge(obj1: JSONValue, obj2: JSONValue): JSONValue {
if (obj1 === null) return obj2
if (type(obj1) !== type(obj2)) return obj2
if (Array.isArray(obj1) && Array.isArray(obj2)) {
const result: JSONValue = [...obj1]
for (let i = 0; i < obj2.length; i++) {
if (isObject(result[i])) {
result[i] = deepMerge(result[i], obj2[i])
} else {
result[i] = obj2[i]
}
}
return result
}
if (isObject(obj1) && isObject(obj2)) {
const result: JSONObject = { ...obj1 }
for (const key in obj2) {
if (key in obj1) {
result[key] = deepMerge(obj1[key], obj2[key])
} else {
result[key] = obj2[key]
}
}
return result
}
return obj2
}type 辅助函数用于区分数组、普通对象以及可能的其他内置类型(尽管 JSONValue 约束了范围,但仍可用于在运行时精确判别)。
基本用法
函数签名:
ts
deepMerge(obj1: JSONValue, obj2: JSONValue): JSONValueobj1:基础对象(左值),会被部分保留。obj2:覆盖对象(右值),优先级更高,决定最终值。- 返回值:一个新的 JSON 值,不修改原始参数。
示例
下面给出若干典型场景及对应的输出,用以直观观察行为。
基本类型覆盖
ts
deepMerge(1, 2) // 2
deepMerge("a", "b") // "b"
deepMerge(true, false) // false
deepMerge(null, 42) // 42
deepMerge(10, null) // null,因为 obj1 非 null,类型不同直接返回 obj2对象合并
ts
deepMerge(
{ name: "Alice", age: 30 },
{ age: 31, city: "Beijing" }
)
// { name: "Alice", age: 31, city: "Beijing" }同名属性 age 被覆盖,obj2 独有的 city 被添加,obj1 独有的 name 得以保留。
嵌套对象合并
ts
deepMerge(
{ config: { db: { host: "localhost" }, cache: true } },
{ config: { db: { port: 5432 } } }
)
// {
// config: {
// db: { host: "localhost", port: 5432 },
// cache: true
// }
// }config.db 内部两个属性都被保留,config.cache 未受干扰。
数组合并
ts
deepMerge(
[1, 2, { a: 1 }],
[7, 8, { b: 2 }, 9]
)
// [7, 8, { a: 1, b: 2 }, 9]索引 0、1 位置为基本类型,直接覆盖为 7、8。索引 2 位置两者都是对象,递归合并。索引 3 是 obj2 多出的项,直接追加。
类型不一致
ts
deepMerge([1, 2], { a: 1 }) // { a: 1 }
deepMerge({ a: 1 }, [1, 2]) // [1, 2]当 obj1 和 obj2 类型不同时,总是返回 obj2,原值被完全取代。
注意点
- null 处理:代码将
null判定为非对象(typeof null === 'object',但isObject排除了null),因此null之间、null与其他类型之间都走规则 1 或规则 5。需留意传入null作为obj1时,返回值一定是obj2,即便obj2也是null。 - 类型判断:
type使用Object.prototype.toString来鉴别数组和对象,这比typeof更准确(typeof []返回"object")。对于JSONValue允许的类型,这样做足够;但若实际传入了超出范围的类型,不会额外报错,而是遵循类型不同的逻辑直接返回obj2。 - 浅拷贝基底:
result总是先浅拷贝obj1,再进行递归赋值。这保证了obj1和obj2不会被修改,符合不可变数据的习惯。但浅拷贝意味着嵌套对象/数组仍然共享引用,后续若有直接修改返回值内引用类型的操作,可能会污染到原始对象。好在本函数只会通过赋值新建或替换,不会直接修改传入对象。 - 数组合并非对称:数组是按照索引一一对应合并的,如果
obj2比obj1短,则obj1多余的项会保留。 - 原型链属性:
for...in会遍历原型上的可枚举属性,但由于参数基于JSONValue(纯对象字面量),原型通常为Object.prototype,影响较小。如果需要严谨,可改用Object.keys或增加hasOwnProperty检查。
限制
值类型仅限于 JSON 可表示的类型:
JSONValue不支持Map、Set、Date、函数等复杂类型。如果传入这些类型,类型检查type(obj1) !== type(obj2)将视为“类型不同”而直接返回obj2,相当于不再递归。对比 Ramda 的mergeDeepRight,后者会处理更多内置类型,例如递归合并Map等。数组与对象的互转:当一个值为数组、另一个为对象时,不会尝试转换,直接替换。这可能与某些配置合并库的行为不同。
数组内嵌数组的递归行为:
isObject仅检查typeof value === 'object' && value !== null,因此对数组返回true。在数组分支中,如果元素是数组,会进入递归合并,而不是整体替换。例如:tsdeepMerge([[1]], [[2]]); // [[2]]执行过程:外层
result = [[1]],遍历obj2[0]即[2],isObject([1])为true,调用deepMerge([1], [2])。内层数组合并,索引 0 为基本类型,直接覆盖为2,返回[2],最终得到[[2]]。这种递归合并遵循数组合并规则,多层嵌套数组会继续深入。如果希望数组内的数组不被递归合并而是当作整体替换,需要修改判断逻辑,但当前设计符合题目行为。
