功能说明
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 属性,确认原型污染键被拦截。