前言
我们都知道,JavaScript是一门弱类型脚本语言,在处理数据复制时涉及到深拷贝和浅拷贝两种不同的概念。这两种拷贝方式在处理对象和数组时有着重要的区别,深拷贝和浅拷贝的选择对于程序的正确性和性能有着深远的影响。下面我将讲解一下深浅拷贝,并尝试着去手写实现一个深拷贝函数。
下面是一道面试题,在我们学习完深浅拷贝知识之后我们再倒过来做一遍。当然,如果你已经会了,显然这篇文章对你的帮助也不会很大。
题目: 实现一个深拷贝函数
deepClone
,该函数能够对传入的对象进行深度复制,确保原始对象与拷贝对象之间没有引用关系。要求:
- 深拷贝应该适用于各种数据类型,包括对象、数组、字符串、数字等。
- 考虑处理循环引用的情况,避免陷入无限递归。
- 你可以选择使用任何合适的方式来实现深拷贝,例如递归、JSON.parse 和 JSON.stringify,或其他方法。
什么是浅拷贝?
浅拷贝是一种复制对象或数组的方法,但它只复制了对象的第一层,而不会递归复制嵌套在其中的对象或数组。简而言之,浅拷贝创建了一个新的对象,但该对象的嵌套结构仍然与原始对象共享引用。对拷贝后的第二层对象的值进行修改,会影响到被拷贝的对象的值。
下面介绍一下JS中一些常见的浅拷贝的方法:
1. Object.create(x)
Object.create()
方法创建一个新对象,该对象的原型链继承自指定的对象x
。
js
let obj = { name: '小明', details:{ age: 25 } };
let objCopy = Object.create(obj);
objCopy.name = '小红';
objCopy.details.age = 30;
console.log(obj.name); // 输出: 小明,原对象未受影响
console.log(obj.details.age); // 输出: 30,原对象受影响
2.Object.assign({},x)
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象{}
。
js
let obj = { name: '小明', details:{ age: 25 } };
let objCopy = Object.assign({},obj);
objCopy.name = '小红';
objCopy.details.age = 30;
console.log(obj.name); // 输出: 小明,原对象未受影响
console.log(obj.details.age); // 输出: 30,原对象受影响
数组的浅拷贝
1.concat
concat
方法用于合并两个或多个数组。它创建一个新的数组,其中包含被调用的数组的浅拷贝。
js
let arr = [1, 2, 3, { value: 4 }];
let arrCopy = arr.concat();
arrCopy[2] = 5;
console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;
console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响
2.slice
slice
返回一个新数组,其中包含被调用的数组的浅拷贝。
js
let arr = [1, 2, 3, { value: 4 }];
let arrCopy = arr.slice();
arrCopy[2] = 5;
console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;
console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响
3.数组解构
- 使用数组解构赋值也能进行浅拷贝
js
let arr = [1, 2, 3, { value: 4 }];
let [...arrCopy] = arr;
arrCopy[2] = 5;
console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;
console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响
什么是深拷贝?
- 递归创建全新对象,对拷贝对象的值进行修改不会影响到原始对象的值。
深拷贝是一种创建对象或数组完全独立副本的方式,包括嵌套在其中的对象或数组。深拷贝确保原始对象和拷贝对象之间不存在引用关系,避免了副作用。
常见的深拷贝方法
JOSN.parse(JSON.stringify(obj))
js
let obj = { name: '小明', value: { a: 1, b: 2 } };
let deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.name = '小红'; // 修改第一层内容
console.log(obj); // 输出:{ name: '小明', value: { a: 1, b: 2 } } 原始对象obj的值没有受影响
deepCopy.value.a = 3; // 修改第二层内容
console.log(obj); // 输出:{ name: '小明', value: { a: 1, b: 2 } } 原始对象obj的值没有受影响
上述方法存在以下缺点:
1.无法拷贝 undefined、function、Symbol、bigint:
JSON.stringify
和JSON.parse
在序列化和反序列化时会丢弃 undefined、function、Symbol,以及不能正确处理 bigint 类型。这意味着深拷贝后的对象可能丧失这些类型的信息。
例子:
js
let obj = { a: undefined, b: function() {}, c: Symbol('symbol'), d: BigInt(123) };
let deepCopy = JSON.parse(JSON.stringify(obj));
console.log(deepCopy); // 输出: { a: null, b: null, c: null, d: 123 }
2.无法处理循环引用:
- 如果对象存在循环引用(即对象的属性之间形成一个闭环),
JSON.stringify
会抛出异常,因为 JSON 格式无法表示循环引用。
例子:
js
let obj = { a: 1 };
obj.self = obj; // 形成循环引用
// 以下代码会抛出异常
// let deepCopy = JSON.parse(JSON.stringify(obj));
手写一个完美的深拷贝
经过上面的分析之后,我们都知道要实现一个完美的深拷贝就必须解决上述常用方法-JOSN.parse(JSON.stringify(obj))
带来的问题。
js
function deepClone(obj, clonedObjects = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检查是否已经拷贝过该对象,避免循环引用导致无限递归
if (clonedObjects.has(obj)) {
return clonedObjects.get(obj);
}
let clone;
// 处理数组
if (Array.isArray(obj)) {
clone = [];
clonedObjects.set(obj, clone);
for (let i = 0; i < obj.length; i++) {
clone[i] = deepClone(obj[i], clonedObjects);
}
}
// 处理对象
else if (obj instanceof Object) {
clone = {};
clonedObjects.set(obj, clone);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], clonedObjects);
}
}
}
return clone;
}
上述方法是如何解决JOSN.parse(JSON.stringify(obj))
带来的问题的呢?这里我们得感谢WeakMap()。
1.使用WeakMap处理循环引用
- WeakMap 是一个弱映射表,它允许你在没有内存泄漏风险的情况下将对象作为键存储在其中。在 deepClone 中,clonedObjects 是一个 WeakMap,用于跟踪已经拷贝过的对象。
为什么不使用Map,而是WeakMap?
使用
Map
替代WeakMap
会导致潜在的内存泄漏问题,因为Map
对键的引用是强引用。而WeakMap
使用的是弱引用,可以避免因为该引用而阻止垃圾回收器对对象的回收。 在深拷贝的场景中,当你使用Map
时,可能会导致整个拷贝的对象及其子对象都无法被垃圾回收,因为Map
对键的引用是强引用。而使用WeakMap
时,当不再存在对原对象的引用时,对应的键值对就可以被垃圾回收,不会造成内存泄漏。
2.对于 undefined、function、Symbol、bigint 的处理:
js
if (obj === null || typeof obj !== 'object') {
return obj;
}
解释:
在进入递归部分之前,首先进行了基础的判断,如果 obj 是 null 或者不是对象类型,就直接返回 obj。这样,对于 undefined、function、Symbol、bigint 这些特殊类型,就会直接复制。
检验是否解决了
1.循环引用
2.对于 undefined、function、Symbol、bigint 的处理