怎么使用展开运算符和 Object.assign 合并 JavaScript 对象
深入讲解如何使用展开运算符(...)与 Object.assign() 合并 JavaScript 对象,剖析合并原理、属性覆盖规则、底层写入机制差异及浅拷贝注意事项。
在日常 JavaScript 开发中,将多个数据源合并为一个完整配置或状态对象是高频操作。合并对象的核心解答主要依赖两种内置方式:对象展开运算符(Spread Operator ...)和 Object.assign() 方法。前者通过简洁的字面量语法生成全新的不可变对象,是现代前端开发的首选;后者则支持对目标对象进行就地更新(In-place Mutation)。两者在同名属性冲突时均遵循“后出现的属性覆盖前面属性”的规则,且均为浅拷贝(Shallow Copy)。
一、使用展开运算符(Spread Syntax)合并对象
对象展开运算符是在 ES2018(ES9)中正式引入的对象特性。在对象字面量内部使用 ... 符号,可以将其后对象的可枚举属性解构并注入到新创建的对象中。
1. 基础合并与属性覆盖
假设系统有一组系统默认参数,同时需要载入调用方传入的自定义参数。我们只需要把两个对象依次放置在新的大括号内:
// 步骤 1:定义基础配置与用户自定义参数
const baseConfig = {
theme: 'light',
pageSize: 20,
showSidebar: true
};
const userConfig = {
theme: 'dark',
pageSize: 50
};
// 步骤 2:使用展开运算符进行合并
const finalConfig = { ...baseConfig, ...userConfig };
console.log(finalConfig);
console.log('是否生成新对象:', finalConfig !== baseConfig);
是否生成新对象: true
在上面的示例中,baseConfig 中的 theme 和 pageSize 被紧随其后的 userConfig 中的同名属性成功覆盖,而未发生冲突的 showSidebar 则被完整保留。最后计算生成的 finalConfig 是一个全新创建的内存对象,原始的 baseConfig 和 userConfig 未发生任何改变。
2. 在合并时追加或覆写行内属性
展开运算符的一大优势在于能够与字面量属性无缝混用。你可以随时在展开前或展开后显式补充新的键值:
const user = { id: 1001, name: 'Alice' };
// 在合并的同时追加 updatedAt,并强制重写 role
const sessionUser = {
...user,
role: 'admin',
updatedAt: '2026-11-01'
};
console.log(sessionUser);
二、使用 Object.assign() 合并对象
Object.assign() 是在 ES2015(ES6)中引入的标准 API,函数签名为 Object.assign(target, ...sources)。它的作用是将一个或多个源对象(sources)自身的可枚举属性复制到目标对象(target)上,并返回该目标对象。
1. 原地修改现有对象
如果直接将既有对象作为第一个参数传入,Object.assign() 会直接更改该对象本身:
const target = { a: 1, b: 2 };
const source = { b: 20, c: 30 };
// target 将被直接修改
const returnedObj = Object.assign(target, source);
console.log(target);
console.log('返回值与 target 引用相同:', returnedObj === target);
返回值与 target 引用相同: true
2. 避免修改源对象:传入空对象作为 Target
当希望像展开运算符一样保持不可变性、生成全新的结果对象时,必须将一个空对象 {} 作为第一个参数传入:
const defaults = { host: 'localhost', port: 8080 };
const envOverrides = { port: 9000, secure: true };
// 关键步骤:第一个参数使用空对象 {}
const appConfig = Object.assign({}, defaults, envOverrides);
console.log(appConfig);
console.log('defaults 保持原样:', defaults.port);
defaults 保持原样: 8080
三、两种合并方式的关键差异与避坑要点
虽然绝大多数日常场景下 { ...a, ...b } 与 Object.assign({}, a, b) 产出的结果看起来完全相同,但在运行机制、属性写入规范及内存引用上存在三个核心差异。
1. 属性赋值机制:[[Set]] 与 [[DefineOwnProperty]]
这是两者在 JavaScript 规范层面上最关键的区别:
Object.assign()使用[[Set]]操作符:它等同于通过点语法赋值(target[key] = value)。若目标对象自身或其原型链上定义了同名属性的setter访问器,该 setter 会被立即执行。- 展开运算符使用
[[DefineOwnProperty]]:它直接在全新创建的对象上新建属性条目,不会沿原型链寻找 setter,因此绝对不会意外触发 setter 钩子。
// 演示 Setter 触发差异
let setterTriggered = false;
const protoWithSetter = {
set token(val) {
setterTriggered = true;
}
};
// Case 1: Object.assign 触发 Setter
const targetA = Object.create(protoWithSetter);
Object.assign(targetA, { token: 'secret-key' });
console.log('Object.assign 触发了 Setter:', setterTriggered); // true
console.log('targetA 自身没有 token 属性:', targetA.hasOwnProperty('token')); // false
// Case 2: 展开运算符始终定义自身属性
const objB = Object.create(protoWithSetter);
const resultB = { ...objB, token: 'secret-key' };
console.log('resultB 拥有自身 token 属性:', resultB.hasOwnProperty('token')); // true
targetA 自身没有 token 属性: false
resultB 拥有自身 token 属性: true
2. 浅拷贝风险:嵌套引用不会被克隆
无论是展开运算符还是 Object.assign(),它们都只执行一层浅拷贝(Shallow Copy)。如果源对象内部包含数组或子对象,合并操作仅仅复制了该子对象的内存引用指针,而没有生成新的副本。
structuredClone()(现代浏览器与 Node.js 17+ 原生支持)或深拷贝工具函数。
const original = {
id: 1,
meta: { author: 'Bob', tags: ['js', 'es6'] }
};
// 浅拷贝合并
const clone = { ...original };
// 修改嵌套对象
clone.meta.author = 'Charlie';
// 检查原对象:受到波及!
console.log('原对象 author 被联动修改:', original.meta.author);
3. 原型链属性与 Symbol 属性处理
在属性范围处理上,两者表现一致:
- 只拷贝自有可枚举属性:继承自原型链(
__proto__)上的属性会被忽略。 - 包含 Symbol 键:源对象上由
Symbol()定义的可枚举键均会被正常合并复制。 - 忽略不可枚举属性:通过
Object.defineProperty()设置为enumerable: false的属性不会参与合并。
四、对比总结与选型指南
为了在实际编码中快速选择最合理的合并策略,可以参考下表列出的核心指标:
| 对比维度 | 对象展开运算符 (...) |
Object.assign() |
|---|---|---|
| 语法风格 | 字面量解构,书写极简,支持行内混用 | 方法调用,语义明确 |
| 返回值特性 | 永远返回全新对象,无就地污染 | 返回 target(若 target 为已有对象则原地修改) |
| 写入机制 | [[DefineOwnProperty]],不触发 Setter |
[[Set]],会触发目标上的 Setter |
| 适用场景 | React/Redux 状态更新、函数式入参组装、常规合并 | 需在已有对象内存引用上就地更新、老版本环境兼容 |
| 深层拷贝支持 | 仅浅拷贝(需搭配 structuredClone) |
仅浅拷贝(需搭配 structuredClone) |
实用选型建议:
- 默认首选展开运算符:在绝大部分前端与 Node.js 业务场景(如组装请求参数、更新 React Component State 或 Pinia Store)中,优先使用
{ ...defaultOptions, ...customOptions },代码可读性最高且具备不可变性保障。 - 需要就地修改时使用
Object.assign:当需要保持现有的对象引用不变(例如更新一个已经传递给多个外部模块的上下文 Context 对象,或者需要在类的构造函数中快速向this挂载属性时),使用Object.assign(this, props)是标准做法。 - 嵌套结构务必警惕:对于多层嵌套的复杂 JSON 数据,浅拷贝合并无法实现子属性的隔离。针对这种场景,应根据业务需要明确采用深拷贝方案。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
相关标签:








