← 中文资源

简体中文版本

RFC 7386 JSON Merge Patch 应用

不修改原对象地应用 RFC 7386 合并补丁,null 表示删除、数组整体替换,并拒绝危险键。

阅读英文原文

功能说明

该示例实现 RFC 7386 的 JSON Merge Patch。补丁是普通对象时递归合并:键值为 null 表示删除目标中的同名键,嵌套对象继续递归,其他值直接替换;补丁本身不是对象时则整体替换目标。数组按整体替换处理,不逐项合并。函数返回新对象,适合配置中心下发增量配置、PATCH 接口更新资源或本地状态合并。

关键代码

isPlainObject 排除 null、数组与自定义类实例,只有原型为 Object.prototype 或 null 的对象才参与递归,防止把 Date、Map 等误当配置对象。顶层结果用展开运算符浅拷贝,未触碰的嵌套值仍共享引用,但被补丁修改的分支会生成新对象。cloneJson 使用 structuredClone 复制替换值,避免调用方之后修改补丁影响结果。键名为 proto、constructor 或 prototype 时直接抛错,阻断原型污染。

使用注意

该算法无法表达把键显式设置为 null,因为 null 在 RFC 7386 中专门表示删除;需要保存 null 时应改用 JSON Patch。补丁必须由 JSON 可表示的值组成,undefined、函数与 Symbol 不属于合法输入。由于未修改的嵌套对象是共享引用,若调用方要求完全不可变,应在应用后再做深度冻结或深拷贝。函数只做结构合并,不校验业务 Schema。

验证方式

用嵌套对象测试新增、覆盖与 null 删除;确认数组被整体替换而不是逐项合并;传入标量补丁验证目标被替换;断言原目标与补丁对象都未被修改;构造含 proto 的补丁应抛 TypeError;对非对象目标应用对象补丁时返回只含补丁字段的新对象。

代码示例

function isPlainObject(value) {
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function cloneJson(value) {
  return value === undefined ? undefined : structuredClone(value);
}

export function applyMergePatch(target, patch) {
  if (!isPlainObject(patch)) return cloneJson(patch);
  const result = isPlainObject(target) ? { ...target } : {};
  for (const [key, value] of Object.entries(patch)) {
    if (key === "__proto__" || key === "constructor" || key === "prototype") {
      throw new TypeError("unsafe patch key: " + key);
    }
    if (value === null) {
      delete result[key];
      continue;
    }
    result[key] = applyMergePatch(result[key], value);
  }
  return result;
}