在 JavaScript 中,对象和数组的赋值默认是引用传递。为了创建副本,我们常使用 Object.assign() 和扩展运算符(...),但它们都只是执行 浅拷贝(Shallow Copy)。
✅ 一、什么是浅拷贝?
浅拷贝是指创建一个新对象,其属性值是对原对象中属性的复制。如果属性值是基本类型,则复制的是值;如果是引用类型(如对象或数组),则复制的是引用地址。
这意味着:
- 基本类型:独立副本;
- 引用类型:共享同一个内存地址;
✅ 二、示例说明:都是浅拷贝
示例 1:使用扩展运算符(...)
let outObj = { inObj: { a: 1, b: 2 } };
let newObj = { ...outObj };
newObj.inObj.a = 2;
console.log(outObj); // { inObj: { a: 2, b: 2 } }
✅ 结果说明:修改了 newObj.inObj.a,outObj.inObj.a 也被改变了,说明 inObj 是引用共享的。
示例 2:使用 Object.assign()
let outObj = { inObj: { a: 1, b: 2 } };
let newObj = Object.assign({}, outObj);
newObj.inObj.a = 2;
console.log(outObj); // { inObj: { a: 2, b: 2 } }
✅ 同样说明:两个对象中的 inObj 指向同一块内存区域,是浅拷贝。
✅ 三、两者的主要区别
| 特性 | Object.assign() | 扩展运算符(...) |
|---|---|---|
| 是否为浅拷贝 | ✅ 是 | ✅ 是 |
| 是否触发 setter | ✅ 是(会调用源对象上的 getter 和目标对象的 setter) | ❌ 否(直接赋值,不触发 setter) |
| 支持继承属性 | ❌ 不复制原型链上的属性 | ❌ 不复制原型链上的属性 |
| 支持 Symbol 属性 | ✅ 是 | ✅ 是 |
| 可读性 | ⚠️ 稍显冗长 | ✅ 更简洁直观 |
| 修改目标对象 | ✅ 是(第一个参数会被修改) | ❌ 否(返回新对象) |
✅ 四、如何实现深拷贝?
由于 Object.assign() 和 ... 都是浅拷贝,对于嵌套对象/数组无法完全隔离引用关系。
实现方式包括:
1. 使用递归函数手动深拷贝(适合简单结构)
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
2. 使用 JSON 序列化(局限性大)
const newObj = JSON.parse(JSON.stringify(oldObj));
⚠️ 缺点:
- 会丢失函数、undefined、Symbol、循环引用等;
- Date 对象会被转成字符串;
3. 使用第三方库(推荐)
lodash.cloneDeep()structuredClone()(现代浏览器支持)immer.js(用于不可变数据更新)
✅ 五、一句话总结
Object.assign()和扩展运算符(...)都是浅拷贝,只复制一层属性。对于嵌套的对象或数组,内部引用仍指向原对象,修改其中一个会影响另一个。
Object.assign()会修改目标对象,并触发 setter;- 扩展运算符不会修改原对象,语法更简洁;
- 如需深拷贝,建议使用递归、JSON 转换或第三方库。
💡 进阶建议
- 在 Vue / React 开发中使用扩展运算符来创建不可变状态;
- 使用 TypeScript 时注意区分
readonly和可变对象; - 使用 ESLint 规则避免误用浅拷贝导致状态污染;
- 推荐使用
lodash.cloneDeep()或structuredClone()实现真正深拷贝;