Appearance
浅拷贝vs深拷贝
1. 概述
在JavaScript中,对象和数组的复制是一个常见而复杂的话题。浅拷贝和深拷贝是两种主要的复制方法,理解它们的区别和使用场景对于编写高质量的代码至关重要。本文将深入探讨这两种拷贝方法的原理、实现和应用,并提供面试中常见的相关问题及其解答。
2. 数据类型与内存分配
在深入浅拷贝和深拷贝之前,我们需要理解JavaScript中的数据类型和内存分配机制。
2.1 基本类型和引用类型
JavaScript中的数据类型可以分为两类:
- 基本类型:Number, String, Boolean, null, undefined, Symbol, BigInt
- 引用类型:Object, Array, Function, Date, RegExp等
2.2 内存分配
- 基本类型:存储在栈(Stack)中
- 引用类型:存储在堆(Heap)中,栈中存储的是指向堆中实际对象的引用
3. 浅拷贝
3.1 定义
浅拷贝创建一个新对象,其属性值是原始对象属性的精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。
3.2 实现方法
- Object.assign()
JavaScript
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
- 展开运算符
JavaScript
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
- Array.prototype.slice()
JavaScript
const original = [1, [2, 3]];
const copy = original.slice();
- 自定义浅拷贝函数
JavaScript
function shallowCopy(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
4. 深拷贝
4.1 定义
深拷贝会递归复制所有层级的属性,创建一个完全独立的新对象,新对象和原对象不共享任何内存。
4.2 实现方法
- JSON.parse(JSON.stringify())
JavaScript
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));
注意:这种方法有局限性,不能处理函数、循环引用、Symbol等特殊情况。
- 递归实现
JavaScript
function deepCopy(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (hash.has(obj)) return hash.get(obj);
const newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key], hash);
}
}
return newObj;
}
- 使用第三方库
- Lodash:
_.cloneDeep()
- jQuery:
$.extend(true, {}, obj)
5. 浅拷贝vs深拷贝
5.1 主要区别
复制深度:
- 浅拷贝:只复制一层
- 深拷贝:递归复制所有层级
内存分配:
- 浅拷贝:共享部分内存(引用类型属性)
- 深拷贝:完全独立的内存
性能:
- 浅拷贝:较快
- 深拷贝:较慢,特别是对于大型复杂对象
使用场景:
- 浅拷贝:适用于只需要复制对象第一层属性的情况
- 深拷贝:需要完全独立的对象副本时使用
5.2 示例比较
JavaScript
const original = { a: 1, b: { c: 2 } };
// 浅拷贝
const shallowCopy = { ...original };
shallowCopy.b.c = 3;
console.log(original.b.c); // 输出: 3
// 深拷贝
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.b.c = 4;
console.log(original.b.c); // 输出: 3
6. 面试题精选
Q1: 什么是浅拷贝和深拷贝?它们的主要区别是什么?
A1: 浅拷贝创建一个新对象,其属性值是原始对象属性的精确拷贝。对于引用类型的属性,浅拷贝只复制引用。深拷贝则会递归复制所有层级的属性,创建一个完全独立的新对象。
主要区别:
- 复制深度:浅拷贝只复制一层,深拷贝递归复制所有层级。
- 内存分配:浅拷贝的新对象与原对象共享部分内存(引用类型属性),而深拷贝创建完全独立的内存。
- 修改影响:修改浅拷贝对象可能影响原对象(针对引用类型属性),而修改深拷贝对象不会影响原对象。
Q2: 如何实现一个深拷贝函数?请考虑各种边界情况
A2: 这是一个可能的实现,考虑了多种边界情况:
JavaScript
function deepCopy(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
const newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj);
// 处理 Symbol 类型的key
const symbolKeys = Object.getOwnPropertySymbols(obj);
if (symbolKeys.length) {
symbolKeys.forEach(symKey => {
newObj[symKey] = deepCopy(obj[symKey], hash);
});
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key], hash);
}
}
return newObj;
}
这个实现考虑了以下边界情况:
- 基本类型和null的处理
- Date 和 RegExp 对象的处理
- 循环引用的处理(使用 WeakMap)
- Symbol 类型键的处理
- 数组的处理
Q3: JSON.parse(JSON.stringify()) 实现深拷贝有什么缺点?
A3: 使用 JSON.parse(JSON.stringify()) 进行深拷贝虽然简单,但有以下缺点:
- 无法处理函数:函数会被忽略。
- 无法处理 undefined:undefined 值的属性会被忽略。
- 无法处理 Symbol:Symbol 类型的属性会被忽略。
- 无法处理 BigInt:会抛出 TypeError。
- 无法处理循环引用:会抛出错误。
- 无法正确处理 Date 对象:会被转换为字符串。
- 无法处理 RegExp、Error 对象:会被转换为空对象。
- 无法处理 NaN、Infinity 和 -Infinity:会被转换为 null。
- 只能序列化可枚举的属性。
Q4: 在React中,如何正确地复制状态对象?为什么不应该直接修改状态?
A4: 在React中,应该使用浅拷贝来复制状态对象,因为React使用浅比较来决定是否重新渲染组件。直接修改状态可能导致组件不更新。
正确的方式:
JavaScript
// 对象
this.setState(prevState => ({
...prevState,
someProperty: newValue
}));
// 数组
this.setState(prevState => ({
arr: [...prevState.arr, newItem]
}));
不应该直接修改状态的原因:
- 违反React的不可变性原则
- 可能导致难以追踪的bug
- 影响性能优化(如 PureComponent 和 React.memo)
- 使得时间旅行调试变得困难
7. 实践建议
- 优先使用浅拷贝:在大多数情况下,浅拷贝足以满足需求,且性能更好。
- 慎用 JSON 方法进行深拷贝:只有在确定对象结构简单,且不包含特殊值(如函数、undefined等)时才使用。
- 考虑使用第三方库:对于复杂的深拷贝需求,考虑使用如 Lodash 的
_.cloneDeep()
方法,它们通常更可靠且性能优化。 - 自定义深拷贝函数:如果经常需要深拷贝,可以根据项目需求自定义一个深拷贝函数,并进行充分测试。
- 在React应用中,遵循不可变性原则:使用展开运算符或
Object.assign()
创建新对象,而不是直接修改现有对象。 - 性能考虑:对于大型对象,考虑是否真的需要完整的深拷贝,可能部分浅拷贝就足够了。
- 测试:编写单元测试来验证你的拷贝函数是否正确处理了所有边界情况。
通过深入理解浅拷贝和深拷贝,你不仅能在面试中表现出色,还能在日常开发中写出更加健壮和高效的代码。记住,选择合适的拷贝方法取决于具体的使用场景和性能需求。