浅拷贝与深拷贝详解:前端对象复制的那些事儿
🎯 引言
前端开发中,操作对象是非常频繁的。但很多小白朋友们在复制对象时容易踩坑,比如修改一个对象的副本,结果却影响了原对象。这篇文章会帮你搞懂对象的引用地址、浅拷贝、深拷贝到底是什么,怎么实现它们,避免不必要的错误,让你的代码更加健壮灵活。
🧩 什么是对象的引用地址?
值类型和引用类型的区别在于它们在内存中存储的方式不同,值类型的数据是存储在栈中,而引用类型的数据是存储在堆中。对象就是属于引用类型。
值类型存储方式
var a = 'xiaoming';
var b = 'libai';
var c = b;
栈模型
| 变量名称 | 类型 | 值 |
|---|---|---|
| a | String | "xiaoming" |
| b | String | "libai" |
| c | String | "libai" |
引用类型存储方式
var a = { name: 'xiaoming', age: 18 };
var b = a;
栈模型
| 变量名称 | 类型 | 值 |
|---|---|---|
| a | Object | 0x000001 |
| b | Object | 0x000001 |
堆模型
| 地址信息 | 属性 name | 属性 age |
|---|---|---|
| 0x000001 | xiaoming | 18 |
⚠️ 直接复制对象会出现什么问题?
如果直接用赋值操作复制对象,实际上只是复制了引用地址,而不是复制对象本身。本质上,两个变量指向的是同一个对象,改动任何一个都会影响到另一个。
这会导致很多难以察觉的 Bug,特别是当复杂对象被多个地方使用时,在某一处对该对象的修改会影响到所有使用它的地方。
✂️ 什么是浅拷贝?
浅拷贝就是复制对象的第一层属性,如果属性值是值类型(字符串、数字等),直接复制值;如果属性值是对象或数组,仍然复制的是它们的引用地址。
举个例子:
var obj1 = {
name: '小明',
hobby: ['篮球', '足球'],
};
var obj2 = Object.assign({}, obj1);
obj2.name = '小红';
obj2.hobby.push('游泳');
console.log(obj1.name); // 小明,值类型改动不影响原对象
console.log(obj1.hobby); // ["篮球", "足球", "游泳"],引用类型改动影响到了原对象
这里 obj2 的 name 字段修改不会影响 obj1,但是 hobby 数组被修改了,原对象也跟着变了。
🔨 如何实现一个浅拷贝?
JavaScript 提供了几个常用方法实现浅拷贝:
- 使用
Object.assign() - 使用扩展运算符(ES6,但本文章避免 ES6 用法)
- 自己写一个循环赋值的函数
示例:用 Object.assign 实现浅拷贝
var source = { a: 1, b: { c: 2 } };
var copy = Object.assign({}, source);
console.log(copy.b === source.b); // true,b 是引用地址相同
🌊 什么是深拷贝?
深拷贝则是递归地复制对象的所有层级,确保复制出来的对象和原对象完全独立,彼此之间不会互相影响。
简单说,就是不仅复制第一层属性,还要复制嵌套的对象、数组,直到最底层的值都是新的。
🛠 如何实现一个深拷贝?
深拷贝要递归地复制所有属性,常见实现方式有:
- 使用
JSON.parse(JSON.stringify(obj)),这是最简单快速的深拷贝方案,但有些限制,比如不能拷贝函数、undefined、循环引用等。 - 自己写递归函数,判断数据类型,逐层复制。
示例:使用 JSON 实现深拷贝
var source = {
a: 1,
b: { c: 2 },
d: function () {
console.log('hello');
},
};
var deepCopy = JSON.parse(JSON.stringify(source));
console.log(deepCopy); // { a: 1, b: { c: 2 } }, 函数被忽略了
下面演示一个简单的递归深拷贝实现:
function deepClone(obj) {
if (obj === null) return null;
if (typeof obj !== 'object') return obj;
var copy = Array.isArray(obj) ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
var source = { a: 1, b: { c: 2 } };
var copy = deepClone(source);
copy.b.c = 3;
console.log(source.b.c); // 2,互不影响
_.cloneDeep。💡 补充知识点:为什么要区分浅拷贝和深拷贝?
在前端开发中,数据状态管理非常重要。比如在 React、Vue 中操作状态时,如果不正确复制对象,可能导致没有触发视图更新,或者出现数据污染。
此外,性能也是考虑因素。深拷贝对大对象很耗性能,浅拷贝虽然不完全复制,但速度快。合理选择拷贝方式,既保证正确性又提高效率。
🧰 实际案例:更新数组中的对象
假设我们有一个用户列表对象数组,需要更新其中某个用户的年龄。
如果直接浅拷贝数组,修改其中一个用户对象,原数组也会改变。用深拷贝就能避免这个问题。
var users = [
{ id: 1, name: '小明', age: 18 },
{ id: 2, name: '小红', age: 20 },
];
var copiedUsers = JSON.parse(JSON.stringify(users));
copiedUsers[0].age = 19;
console.log(users[0].age); // 18,原数组不受影响
🧾 小节总结
- 对象变量存储的是引用地址,复制时要注意引用共享问题
- 浅拷贝只复制第一层属性,引用类型仍指向同一地址
- 深拷贝递归复制所有层级,确保数据完全独立
- 简单快速深拷贝可用
JSON.parse(JSON.stringify()),但有局限 - 复杂应用建议用递归函数或成熟库实现深拷贝,避免循环引用陷阱
❓ 知识问答(Q&A)
Q:浅拷贝会复制对象里的函数吗?
A:浅拷贝会复制函数的引用,函数本身不会被复制,引用还是同一个。
Q:为什么用 JSON.parse(JSON.stringify()) 不能深拷贝函数?
A:因为 JSON 格式不支持函数类型,序列化时会被忽略。
Q:有没有简单方法检测对象是否有循环引用?
A:可以借助 WeakSet 来检测遍历过程中是否访问过某个对象,避免死循环。
🧪 小练习
练习 1:请使用 Object.assign 实现一个简单的浅拷贝函数,并测试修改值类型和引用类型属性对原对象的影响。
function shallowCopy(obj) {
// 请在这里实现代码
}
var original = { name: '小明', hobby: ['篮球', '足球'] };
var copy = shallowCopy(original);
copy.name = '小红';
copy.hobby.push('游泳');
console.log(original.name); // ?
console.log(original.hobby); // ?
练习 2:写一个递归函数,实现深拷贝,注意避免循环引用导致死循环。写完后测试复制一个多层嵌套对象。
function deepClone(obj, hash) {
// 请在这里实现代码
}
var complexObj = { a: 1, b: { c: 2, d: { e: 3 } } };
var copy = deepClone(complexObj);
copy.b.d.e = 4;
console.log(complexObj.b.d.e); // ?
console.log(copy.b.d.e); // ?
🎉 恭喜你已经掌握了对象引用地址、浅拷贝、深拷贝的核心知识和实现方法啦!这对写出更安全、性能更优的前端代码帮助很大,继续加油吧!
