← 中文资源

简体中文版本

安全地深合并普通对象

递归合并嵌套配置对象且不修改输入,克隆数组和特殊值,并拦截原型污染键。

阅读英文原文

功能说明

deepMerge 接收任意数量的普通对象,从左到右递归合并,返回一个全新的对象。嵌套的普通对象会继续合并,数组、Date、Map、Set 以及其他值会被克隆或直接复制,因此修改结果不会影响输入。后面的来源覆盖前面的同名标量;值为 undefined 的键会被忽略,方便用可选配置覆盖默认值。

实现专门过滤 __proto__、constructor 和 prototype 三个危险键,避免把 JSON.parse 得到的恶意对象合并进结果后污染全局原型。它适用于加载默认配置、环境配置和用户覆盖项,也适合构造嵌套的请求参数。

关键代码

合并函数只接受普通对象作为来源,并对每个值判断类型。普通对象会与目标中的同名对象继续递归合并,其他值通过 cloneValue 复制。

function mergeInto(target, source) {
  for (const [key, value] of Object.entries(source)) {
    if (DANGEROUS_KEYS.has(key) || value === undefined) continue;
    if (isPlainObject(value)) {
      const base = isPlainObject(target[key]) ? target[key] : {};
      target[key] = mergeInto({ ...base }, value);
    } else {
      target[key] = cloneValue(value);
    }
  }
  return target;
}

isPlainObject 只接受原型为 Object.prototype 或 null 的对象,因此类实例不会被误当成配置对象展开。

使用注意

该实现不处理循环引用;配置对象应当是可序列化的树状结构,若来源存在自引用会无限递归。数组采用替换语义而不是按下标合并,最后出现的数组会整体覆盖前面的数组并复制一份。Map 和 Set 只克隆值,不递归合并键或元素内部的普通对象。

Date 会被复制为新的时间对象,但其他类实例按引用保留,调用方不应依赖深拷贝语义。危险键被静默跳过,不会报错;如果希望显式拒绝恶意输入,应在调用前单独校验。函数和 Symbol 值会原样保留,但不会被 JSON 序列化。多个来源中出现 null 时会用 null 覆盖旧值,这是有意的替换行为。

验证方式

示例把默认的服务器、标签和限流配置与覆盖项合并,再追加一个只改 host 的来源,输出中端口为 8080、TLS 为 true、host 为 0.0.0.0,同时原 defaults.server.port 仍为 3000,证明没有修改输入。最后用包含 __proto__ 的 JSON 合并,检查结果对象和空对象都没有 polluted 属性,确认原型污染键被拦截。