Object.assign合并JS对象的用法与注意事项
全面讲解 JavaScript 中 Object.assign() 合并对象的用法与底层运行机制。涵盖基本语法、多源属性覆盖、浅拷贝原理、Symbol 支持、Getter/Setter 触发规则及常见陷阱与避坑指南。
Object.assign() 是 JavaScript 用于将一个或多个源对象(source)自身的可枚举属性复制到目标对象(target)的标准 API。它在日常业务中常用于对象合并、默认配置项初始化以及对象的浅克隆。然而,由于它采用原地修改目标对象、仅执行浅层引用复制并通过普通赋值触发 Getter/Setter,在实际开发中极易出现数据联动污染、嵌套属性被整体覆写等隐蔽 Bug。本文将从基础语法、属性覆盖规则、内存引用模型,到高级边界陷阱与替代方案,逐一剖析具体用法与核心注意事项。
一、Object.assign 的基本语法与就地修改特性
Object.assign() 接受一个目标对象和一个或多个源对象,将所有源对象自身的有效属性逐一复制到目标对象中。其标准语法形式如下:
Object.assign(target, ...sources);
使用该方法时,读者首先需要明确的第一个核心概念是:目标对象 target 会被就地直接修改(Mutated in place),并且该方法的返回值就是修改后的 target 本身,二者在内存中持有完全相同的引用地址。
const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);
console.log(target); // 输出: { a: 1, b: 2 }
console.log(result === target); // 输出: true (引用完全相同)
true
正因为这个特性,如果直接将现有业务对象作为 target 传入,该对象原有的属性结构就会被改变。为了避免污染原始对象,开发中通常会传入一个空对象字面量 {} 作为初始目标:
const original = { name: "Alice", score: 90 };
// 将空对象作为 target,生成全新对象,保护 original 不受修改
const safeCopy = Object.assign({}, original, { score: 100 });
console.log(original.score); // 依然保持 90
console.log(safeCopy.score); // 输出: 100
从上述执行流程图可以看出,Object.assign 在底层并不是黑盒镜像复制,而是分阶段逐步运行:先验证目标对象的有效性,接着依次遍历每个源对象,通过内部 [[Get]] 机制读取数值,再借助 [[Set]] 写入目标。理解这个机制,是深入理解后续属性覆盖与陷阱的关键。
二、多源合并与属性覆盖规则
当开发者同时传入多个源对象(例如 Object.assign(target, source1, source2, source3))时,JavaScript 引擎会严格按照从左到右的参数顺序依次提取属性并执行合并。
1. 靠后属性优先覆盖(Latter Overwrites Former)
如果多个源对象中存在相同名称的属性键,排在后面的源对象属性值将无条件覆盖前面源对象或目标对象中的对应值。
const target = { status: "init", retries: 0 };
const s1 = { status: "loading", timeout: 3000 };
const s2 = { status: "success", retries: 3 };
Object.assign(target, s1, s2);
console.log(target);
在上面的示例中,status 经过了 "init" ➔ "loading" ➔ "success" 的连续覆盖,最终以最靠后的 s2 结果为准。
2. 原型链继承属性与不可枚举属性会被自动忽略
Object.assign() 仅拷贝源对象自身可枚举(Own Enumerable)的属性。这意味着:
- 源对象原型链(Prototype Chain)上继承而来的方法或属性,不会被复制到目标对象中。
- 通过
Object.defineProperty()定义且enumerable: false的私有/不可枚举属性,同样会被忽略。
3. 对 Symbol 类型属性键的完整支持
许多初学者误以为 Object.assign() 只能拷贝常规字符串键名。事实上,ES6 标准明确规定,源对象中键名为 Symbol 类型的自身可枚举属性,也会被完整拷贝到目标对象中。
const symId = Symbol("unique_id");
const sourceWithSymbol = {
[symId]: "SECRET_TOKEN_9988",
title: "Doc"
};
const merged = Object.assign({}, sourceWithSymbol);
console.log(merged[symId]); // 正常输出: "SECRET_TOKEN_9988"
4. 原始类型传入时的特殊表现
当非普通对象作为参数传入 Object.assign 时,JavaScript 会按特定规则处理:
- 若源参数为
null或undefined:会被引擎安全忽略,不会抛出错误。 - 若源参数为原始字符串:字符串会被自动包装为 String 对象,其每个字符对应的数字索引属于可枚举自身属性,因此会被拆解并按索引拷贝(例如
"abc"会变成{ 0: "a", 1: "b", 2: "c" })。 - 若源参数为数值或布尔值:由于包装后的 Number / Boolean 对象没有任何自身可枚举属性,会被静默跳过。
- 若
target参数为null或undefined:会直接抛出TypeError: Cannot convert undefined or null to object,因为它们无法转换为 Object。
三、核心注意事项:浅拷贝与引用共享陷阱
在所有关于 Object.assign 的问题中,最容易在实际项目中引发 Bug 的就是浅拷贝(Shallow Copy)行为。
1. 嵌套对象的地址引用复制
Object.assign() 拷贝的只是对象第一层键值对。如果某个属性的值是原始类型(数字、字符串、布尔值),拷贝过去的是真实的值;但如果该属性的值是一个引用类型(对象、数组、Map 等),拷贝过去的其实只是该对象在堆内存中的内存地址指针。
const user = {
id: 101,
profile: {
city: "Beijing",
role: "editor"
}
};
const userCopy = Object.assign({}, user);
// 尝试修改克隆对象的内层属性
userCopy.profile.city = "Shanghai";
// 查看原对象的属性:原对象被一同篡改了!
console.log(user.profile.city); // 输出: "Shanghai"
从上面的结果可以看到,虽然外层 userCopy !== user,但 userCopy.profile === user.profile。修改任何一方的嵌套属性,另一方都会同步改变。在管理表单初始值、状态回滚或 Redux / Vue 状态管理时,这往往会导致灾难性的状态污染。
2. 嵌套对象不会递归深度合并,而是整体替换
部分开发者直觉上期望 Object.assign 能像“配置补全”一样对深层对象进行逐字段合并,但实际上它完全不支持深层合并。一旦源对象包含同名嵌套对象,后者的对象引用会直接整体覆盖掉前面的嵌套对象。
const defaultSettings = {
network: { timeout: 5000, keepAlive: true }
};
const customSettings = {
network: { timeout: 10000 } // 没有写 keepAlive
};
const finalConfig = Object.assign({}, defaultSettings, customSettings);
console.log(finalConfig.network);
// 结果是 { timeout: 10000 },原有的 keepAlive 丢失了,并没有被保留!
如果需要对多层嵌套对象进行深层递归合并,应使用成熟的深层合并工具(如 Lodash 的 merge 函数)或手写递归深合并。
四、高级注意事项与边界条件
1. Getter 与 Setter 的触发机制
Object.assign() 在执行复制时,使用的是源对象的常规属性读取(触发内部 [[Get]])以及目标对象的常规属性赋值(触发内部 [[Set]])。这意味着它不会复制属性的描述符(Property Descriptor):
- 如果源对象上定义了一个 Getter 访问器函数,
Object.assign会立刻调用执行该 Getter,并把计算返回的具体值赋给目标属性,而不会把 Getter 函数本身拷贝过去。 - 如果目标对象上定义了一个 Setter 函数,属性复制过程中会触发目标对象的 Setter 方法。
const sourceWithGetter = {
_val: 10,
get doubleVal() {
return this._val * 2;
}
};
const target = Object.assign({}, sourceWithGetter);
console.log(Object.getOwnPropertyDescriptor(target, "doubleVal"));
// 输出: { value: 20, writable: true, enumerable: true, configurable: true }
// doubleVal 已经变成了普通的静态数值 20,失去了 getter 计算能力
若需要完整复制包含 Getter/Setter、不可枚举属性以及属性特性的完整描述符,应当使用 ES2017 提供的 Object.defineProperties() 配合 Object.getOwnPropertyDescriptors():
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
2. 异常中断与不可逆修改(非原子性操作)
Object.assign() 的属性赋值过程不是一个原子事务。如果复制过程中途抛出异常(例如试图写入目标对象上的只读属性 writable: false),整个赋值流程会立即中断退出。
但请注意:在发生异常之前已经成功赋值的所有属性,依然保留在目标对象中,不会自动回滚!
const target = Object.defineProperty({}, "readOnlyKey", {
value: 42,
writable: false
});
try {
Object.assign(target, { step1: "OK" }, { readOnlyKey: 999 }, { step2: "OK" });
} catch (e) {
console.error("合并中断:", e.message);
}
console.log(target.step1); // 输出: "OK" (在报错前已被成功赋值)
console.log(target.step2); // 输出: undefined (报错后被跳过)
五、实际开发中的常见应用场景与方案选型
1. 合并默认配置选项(Options Merging)
这是 Object.assign 最经典的应用场景。编写函数或组件时,提供基础默认配置,并允许调用方按需传入自定义选项:
function createHttpClient(customOptions = {}) {
const defaultOptions = {
baseURL: "https://api.example.com",
timeout: 3000,
headers: { "Content-Type": "application/json" }
};
// 注意:如果 options 存在深层嵌套,需对深层另外做安全合并
return Object.assign({}, defaultOptions, customOptions);
}
2. 为类实例或原型扩展混入方法(Mixins)
在面向对象编程中,利用 Object.assign 能够非常轻量地将通用能力模块组合到现有类或原型链中:
const eventEmitterMixin = {
emit(event, data) { console.log(`Trigger [${event}]:`, data); }
};
class UserService {}
// 将事件发射能力混入 UserService 原型
Object.assign(UserService.prototype, eventEmitterMixin);
const service = new UserService();
service.emit("userLogin", { uid: 1001 }); // 成功调用混入的方法
3. 现代替代方案对比:展开运算符 与 structuredClone
在现代 JavaScript (ES2018+) 开发中,除了 Object.assign,开发者还可以根据实际场景选择更为合适的方案:
- 对象展开运算符
{ ...a, ...b }:语法更为简洁流畅,且保证始终返回全新对象,非常适合 React / Redux 等不可变数据流开发。需要注意的是,对象展开运算符同样也是浅拷贝。 - 原生
structuredClone()方法:现代浏览器和 Node.js 17+ 原生内置的标准深拷贝 API。能够真正递归拷贝多层嵌套对象、数组、Map、Set 以及循环引用,彻底告别引用共享隐患。
六、总结与特性速查表
最后,我们将 Object.assign() 的核心特性与关键注意事项归纳为以下速查清单,供日常排查与技术方案设计时参考:
| 特性维度 | 表现与行为 | 开发注意事项 |
|---|---|---|
| 目标影响 | 直接就地修改 target 对象并返回该对象引用 |
若要实现不污染源数据的浅拷贝,第一个参数必须传入 {} |
| 拷贝深度 | 浅拷贝(仅拷贝自身第一层属性) | 嵌套对象属性仅复制内存地址指针,深层修改会产生联动副作用 |
| 覆盖规则 | 参数从左到右遍历,同名键由靠后的源对象无条件覆盖 | 深层对象同名时会整体替换,不会自动进行递归深合并 |
| Symbol 支持 | 支持自身可枚举的 Symbol 键名 |
与普通字符串键名享受相同的覆盖与拷贝逻辑 |
| 原型与不可枚举 | 原型链继承属性、enumerable: false 属性均被自动忽略 |
需要保留完整属性元数据时应使用 Object.defineProperties |
| Getter / Setter | 触发源对象的 Getter 读取值,触发目标对象的 Setter 赋值 | 不会复制访问器函数自身,复制过去的是执行 Getter 后的静态结果 |
| 异常机制 | 非原子事务,遇只读属性抛错立即中断 | 报错前已完成赋值的属性不会回滚,可能产生半更新脏数据 |
掌握 Object.assign() 的关键在于厘清其“浅拷贝”、“原地修改”与“基于值的读写”三大基石特性。在需要浅层合并与实例修改时善用该方法,在需要深层数据隔离时选用 structuredClone(),在不可变状态流中优先使用展开运算符,方能编写出健壮、高效且易于维护的 JavaScript 代码。
大型前端项目该选什么JavaScript编辑器?
2026-11-05 18:32:43
JavaScript正则替换中美元符号$与反斜杠的转义处理
2026-11-05 18:23:52
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29








