1. 先还原一次翻车现场:为什么原对象被悄悄改掉了
先把话说在前面:Object.assign() 这个 API,我在项目里见过太多人用错。最常见的误解是“它能把一个对象复制出一个新对象”,但实际行为完全不是这样。这里我先说结论,再带你复现一遍。
1.1 最小可复现案例
假设你有两份配置,一份是默认配置,一份是用户传入的覆盖配置,想合并成一份最终配置。第一反应写出来的代码往往是这样的:
javascript复制const defaultConfig = {
theme: 'light',
fontSize: 14,
};
const userConfig = {
theme: 'dark',
};
const finalConfig = Object.assign(defaultConfig, userConfig);
console.log(finalConfig);
// { theme: 'dark', fontSize: 14 }
console.log(defaultConfig);
// { theme: 'dark', fontSize: 14 } ← 原对象也被改了!
如果你觉得 finalConfig 是一个“新对象”,那这个输出会直接打破你的认知:defaultConfig 怎么会变成 { theme: 'dark', fontSize: 14 }?最扎心的是,你检查 finalConfig === defaultConfig,结果会是 true——它们就是同一个对象。
这就是我常说的“第一个坑”。Object.assign() 不是“复制到新对象”,而是“把源对象的属性赋值给目标对象”,目标对象会被原地修改,然后返回它。你可以这么理解:你把水果装进一个篮子里,这个篮子本来就是你自己的,装完之后它还是那个篮子,只是里面的东西变了。你要的是“另一篮子水果”,但代码给的是“你自己篮子换了一批水果”。
1.2 返回值不是“新对象”:如何优雅地不污染原对象
那么问题来了:日常合并配置时,绝大多数情况下我们不想改掉默认配置对象。解决办法很简单——把目标对象指定成一个全新的空对象:
javascript复制const finalConfig = Object.assign({}, defaultConfig, userConfig);
console.log(defaultConfig);
// { theme: 'light', fontSize: 14 } ← 原对象原封不动
console.log(finalConfig);
// { theme: 'dark', fontSize: 14 }
看到没有,第一个参数传 {},defaultConfig 就不会被污染。这也是为什么所有教程里写 Object.assign 时,几乎都会带着一个空对象 {} 当靶子。记住一条经验:只要看到 Object.assign 的第一个参数不是 {},就要停下来想一想,这个目标对象是不是真的允许被修改。
另外,因为返回值就是第一个参数,所以 Object.assign 支持链式调用。比如:
javascript复制const state = Object.assign(
Object.assign({}, baseState, { loading: true }),
{ error: null }
);
这种写法在老代码里偶尔能看到,本质上是把上一次合并的结果继续当作目标对象。理解了“返回值等于目标对象”这件事,就不会对着这种代码发懵。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从命名到合并规则:assign 到底在 assign 什么
很多人把 Object.assign 当“黑盒”用,但我建议把它的名字拆开看:Object 是一个静态方法容器,assign 的意思是“分配、指派”。连起来,就是把一个或多个源对象的属性,一个一个“指派”给目标对象。
2.1 它挂在 Object 上的静态方法,不是对象按钮
有个新手常见疑问:为什么我访问 obj.assign 是 undefined?因为 Object.assign 是定义在 Object 构造函数上的静态方法,不是挂载在每个对象实例上的。你需要写 Object.assign(target, source),而不能直接写 target.assign(source)。这和 obj.toString() 这种通过原型链调用的对象方法是不同的。
你可以这么区分:凡是 Object.xxx() 这种形式,一般是工具函数,处理对象相关的通用逻辑;凡是 obj.xxx() 这种形式,一般是实例方法,操作当前对象本身。Object.assign 属于前者。
2.2 多对一合并:后面的源对象优先级更高
Object.assign 支持一次传入多个源对象,关键规则是:如果几个源对象里有相同的属性名,后面的源对象会覆盖前面的源对象。
javascript复制const result = Object.assign(
{},
{ a: 1, b: 2 },
{ b: 3 },
{ c: 4 }
);
console.log(result);
// { a: 1, b: 3, c: 4 }
看到这里你应该能理解为什么它适合做“默认配置 + 用户配置”的合并:把默认配置放前面,用户配置放后面,用户传了什么就覆盖什么,没传的就保留默认值。这种“后面的赢”的规则,在 JavaScript 里其实很常见,比如 CSS 里后面的样式会覆盖前面的,同一个思路。
2.3 复制属性时的筛选规则
Object.assign 不是“把所有东西都搬过去”,它只复制源对象的可枚举自有属性。什么叫可枚举?你可以简单理解为一个属性在 for...in 或 Object.keys() 的遍历中能不能被看到。什么叫自有?就是这个属性定义在对象自己身上,而不是从原型链上继承下来的。
| 场景 | 是否会被 Object.assign 复制 |
|---|---|
| 可枚举的自有字符串属性 | 是 |
| 可 |
