前言
在处理对象或数组时,我们经常遇到“改了 A,结果 B 也变了”的情况。这背后涉及到了内存地址、引用传递以及拷贝的深度问题。理解浅拷贝与深拷贝,是处理复杂数据流和状态管理(如 Redux, Vuex)的基础。
一、 核心概念对比
1. 引用赋值
直接使用 =,这不属于拷贝,只是让两个变量指向内存中的同一个地址。
let obj1 = { a: 1 };
let obj2 = obj1; // 引用赋值
obj2.a = 2;
console.log(obj1.a); // 2 (相互影响)
2. 浅拷贝 (Shallow Copy)
创建一个新对象,拷贝其第一层属性。如果属性是基本类型,拷贝的是值;如果属性是引用类型,拷贝的是内存地址。
- 实现方式:
Object.assign()、扩展运算符...、Array.prototype.slice()。
3. 深拷贝 (Deep Copy)
创建一个新对象,递归地拷贝所有层级的属性。两个对象在内存中完全独立,互不影响。
二、 深拷贝的两种主流实现
方法 1:JSON 序列化(简单但有陷阱)
思路:JSON.parse(JSON.stringify(obj))
-
优点:简单快捷,一行代码搞定。
-
缺点(面试常考点) :
- 会忽略
undefined和symbol。 - 会忽略
function(函数无法被序列化)。 Date对象会变成字符串。- 无法处理循环引用的对象(会报错)。
- 会忽略
const obj1 = {
body: { a: 10 },
say: function(){ console.log('hello') }
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2.say); // undefined (函数丢失了!)
方法 2:递归手动实现(面试必考)
要写出一个完美的 deepClone,需要考虑:数组兼容、递归调用、原型属性过滤。
/**
* 深拷贝递归实现
* @param {Object} target 目标对象
* @returns {Object}
*/
function deepClone(target) {
// 1. 如果不是对象或者是 null,直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
// 2. 初始化返回结果(判断是数组还是对象)
const result = Array.isArray(target) ? [] : {};
// 3. 遍历目标对象
for (let key in target) {
// 4. 确保只遍历对象自身的属性,不包含原型链
if (target.hasOwnProperty(key)) {
// 5. 如果子属性还是对象,递归调用
if (target[key] && typeof target[key] === 'object') {
result[key] = deepClone(target[key]);
} else {
// 6. 基本类型则直接赋值
result[key] = target[key];
}
}
}
return result;
}
// 测试
const original = { name: 'ouyang', arr: [1, 2], fn: () => {} };
const cloned = deepClone(original);
cloned.arr[0] = 999;
console.log(original.arr[0]); // 1 (互不影响,成功!)
三、 面试模拟题
Q1:Object.assign() 是深拷贝还是浅拷贝?
参考回答: 是浅拷贝。它只拷贝源对象自身的可枚举属性到目标对象。如果源对象的属性值是一个指向对象的引用,它也只拷贝那个引用地址。
Q2:如何解决深拷贝中的“循环引用”问题?
参考回答: 在递归实现中,可以使用一个 WeakMap 来存储已经拷贝过的对象。每次拷贝前先在 WeakMap 中查找,如果已经存在,则直接返回该对象,避免死循环。
Q3:为什么 JSON.stringify 无法拷贝函数?
参考回答: 因为 JSON 是一种数据交换格式,其标准中只定义了数字、字符串、布尔值、数组、对象和 null。函数属于代码逻辑而非数据,因此在 JSON 序列化规范中被排除在外。
四、 总结:我该选哪种方案?
- 处理简单的纯数据:用
JSON.parse(JSON.stringify(obj)),效率最高。 - 处理包含函数或特殊对象的复杂数据:使用手写的
deepClone或者成熟的第三方库如 Lodash 的_.cloneDeep()。 - 高性能需求:如果数据量极大且只有一层,优先使用扩展运算符
{...obj}。