Object.assign只能做浅拷贝,因其仅遍历源对象的可枚举自有属性并逐个赋值,不递归处理嵌套对象,引用类型仅复制地址;它忽略不可枚举属性和Symbol键,不处理特殊对象(如Date、Map)的深拷贝。

Object.assign 为什么只能做浅拷贝
Object.assign 内部遍历源对象的**可枚举自有属性**,逐个赋值到目标对象上。它不递归处理嵌套对象,遇到引用类型(比如 {a: {b: 1}})时,只是把那个 Object 的引用地址拷过去。所以修改拷贝后对象里的嵌套属性,原对象也会跟着变。
常见错误现象:
执行 const a = {user: {name: 'x'}}; const b = Object.assign({}, a); b.user.name = 'y'; console.log(a.user.name) → 输出 'y',说明 a.user 和 b.user 指向同一块内存。
这不是 bug,是设计使然 —— Object.assign 定位就是“属性级复制”,不是深拷贝工具。
合并多个配置对象时的覆盖顺序和规则
调用形式是 Object.assign(target, ...sources),参数从左到右依次处理:前面的源对象属性会被后面同名属性覆盖,且**不会跳过 undefined 或 null**。
- 如果某个源对象为
null或undefined,会被跳过(不报错) - 如果某个源对象是原始值(如
42、'str'),会被转成对应包装对象(Number、String),但它们没有可枚举属性,所以等效于空对象 - 属性覆盖按参数顺序:第 2 个参数的同名属性会覆盖第 1 个,第 3 个又覆盖第 2 个,以此类推
示例:
const defaults = {host: 'localhost', port: 3000, debug: false};
const envConfig = {port: 8080};
const cliConfig = {debug: true, timeout: 5000};
const config = Object.assign({}, defaults, envConfig, cliConfig);
// 结果:{host: 'localhost', port: 8080, debug: true, timeout: 5000}
容易被忽略的不可枚举属性和 symbol 键
Object.assign 只处理**可枚举的自有字符串键**,两个关键遗漏点:
- 不可枚举属性(比如用
Object.defineProperty设置enumerable: false的)完全不会被拷贝 -
Symbol类型的键默认也不会被拷贝,即使它是可枚举的
这意味着如果你依赖 Symbol 做私有配置字段(比如 config[Symbol.for('meta')]),用 Object.assign 合并时它就丢了。需要手动处理或换用 Reflect.ownKeys + 循环。
替代方案与边界提醒
当你要合并配置且希望更可控,或必须保留 Symbol / 不可枚举属性时,Object.assign 就不够用了。这时候可以:
- 用展开运算符
{...defaults, ...envConfig, ...cliConfig}—— 行为几乎一致,但更简洁;同样不处理Symbol和不可枚举属性 - 用
Object.getOwnPropertyDescriptors+Object.defineProperties手动控制所有属性特性(包括enumerable、writable、configurable) - 对深嵌套配置,直接上
structuredClone(现代环境)或专用库(如lodash.merge),别硬改Object.assign
最常被低估的一点:很多人以为 Object.assign({}, obj) 是“安全复制”,但只要 obj 里有 Date、RegExp、Map、Set 这类对象,拷出来的只是它们的原始值表现(比如 Date 变成字符串),甚至丢失原型链 —— 这些都不是“拷贝”,而是“强制类型转换”。










