Object.assign 合并配置时真正风险是原型污染和不可枚举属性被忽略;它只复制可枚举自有属性,不处理原型链,需用 Object.create(null) 隔离或手动过滤危险键。

用 Object.assign 合并配置时,系统属性(如 __proto__、constructor、toString 等)不会“丢失”,但可能被意外注入或覆盖——真正风险是**原型污染**和**不可枚举属性被忽略**。Object.assign 只复制可枚举的自有属性,它本身不处理原型链,也不会主动删掉系统属性;问题出在:你把含危险键的用户输入直接当源对象传入,或误以为默认对象能“兜底”所有继承行为。
明确默认对象只管自有可枚举字段
默认参数对象的作用,是提供基础键值对,供后续配置覆盖。它无法保护原型、不能恢复不可枚举属性,也不能阻止恶意键写入目标对象。
- 默认对象应精简、白名单化:只包含你明确需要的配置项,比如
{ timeout: 5000, retries: 3, method: 'GET' } - 避免在默认对象里塞函数或访问器:Object.assign 会执行 getter 并存为普通值,行为语义丢失
- 不要依赖默认对象“修复”原型——它压根不读取原型,更不会补全
用 Object.create(null) 隔离原型污染风险
最有效防止系统属性干扰的方式,是让目标对象彻底无原型。这样 __proto__、constructor 等根本不存在,自然不会被覆盖或触发污染。
- 安全写法:
Object.assign(Object.create(null), defaults, userConfig) - 适合纯数据场景(如配置中心、路由参数、表单校验规则),结果是干净的 plain object
- 注意:返回对象不再有
toString、hasOwnProperty等方法,需改用Object.prototype.hasOwnProperty.call(obj, key)等方式检测
手动过滤危险键,不依赖“默认兜底”
默认对象不能拦住恶意键,必须在合并前清理源对象。
- 对来自 JSON.parse、表单、URL 参数的
userConfig,先做白名单过滤:const safeConfig = Object.fromEntries(Object.entries(userConfig).filter(([k]) => !['__proto__', 'constructor', 'prototype'].includes(k))) - 或者用
Object.keys()替代Object.assign()手动赋值,跳过黑名单键 - 永远别写
Object.assign(target, userInput)—— 这是原型污染高发写法
需要保留原型行为?那就别用 Object.assign 做“合并”
如果你的配置对象本就继承自某个构造器(比如 class ApiConfig extends BaseConfig),那 Object.assign 的扁平化行为会切断原型链,导致方法、默认值、响应式代理全部失效。
- 正确做法:用
Object.setPrototypeOf(Object.assign({}, instance), Object.getPrototypeOf(instance))重建链 - 或者直接操作实例:
Object.assign(instance, userConfig),但前提是确保instance是受信对象且已做键过滤 - Vue/React 场景中,合并后还需重新 reactive() 或 setState,否则响应性丢失

















