Skip to content

2755. 深度合并两个对象

2755. 深度合并两个对象

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'
}

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
}

合并规则

deepMerge 的语义可以看成 Object.assign 再叠加一层深度递归:

  1. obj1 为 null → 返回 obj2。 null 被视为「无值」,obj2 直接覆盖
  2. 类型不同 → 返回 obj2。 如果一个是数组一个是对象,无法合并,obj2 覆盖
  3. 都是数组 → 按索引合并。 obj2 的元素覆盖 obj1 的同索引元素;如果 obj2 更长,多余的元素追加
  4. 都是对象 → 按 key 合并。 同名 key 递归合并;仅在 obj2 中存在的 key 直接添加
  5. 基本类型 → 返回 obj2。 数字、字符串等直接覆盖

展开运算符的策略

const result: JSONObject = { ...obj1 } 先复制 obj1 的所有属性,这是一个浅拷贝。然后遍历 obj2 的 key,同名 key 递归合并,新 key 直接赋值。这样 obj1 中 obj2 没有的 key 得以保留——这是「合并」而非「替换」。

数组的处理不同:const result: JSONValue = [...obj1] 同样浅拷贝,然后逐索引处理。obj2[i] 存在而 result[i] 是对象时,递归合并;否则直接覆盖。

与 Ramda.mergeDeepRight 的对比

Ramda 的 mergeDeepRight(或 mergeDeepLeft)有明确的偏向规则,right 覆盖 left。这里的实现也是 obj2 优先,效果上更接近 mergeDeepRight。区别在于 Ramda 还会处理 MapSet、函数等类型,而这里的 JSONValue 约束把输入范围收得更窄。