深浅拷贝可谓是面试中高频考点,那么本期就详细讲一讲这个令人头疼的问题~本文涉及数据结构的讲解,深浅拷贝的区别,举例,算法,以及优缺点。文章较长,如果哪里你会,可以跳着看,汲取自己需要的即可~~
一、数据结构中的堆和栈
老规矩---先撸定义:
深拷贝实际上是在内存中开辟一块新的地址来存放复制的对象。(简单来说就是两块地方,谁都不影响谁)
浅拷贝实际上是 只拷贝一层 更深层对象级别只拷贝地址。(指针指向同一个内存地址)
接下来咱们还是先普及数据结构,以防止小白同学一头雾水,我比较懒没画图,在网上找的图,拿来用一下,给大家讲解。
首先,js中数据类型分为基本数据和引用数据类型。 基本数据类型(Undefined,String,Boolean,Null,Number)都是存放在栈中,它遵循后进先出(LIFO)的原则。在 JavaScript 中,当我们声明一个基本类型的变量时,JavaScript 引擎会在栈中为该变量分配一块内存空间,并将变量的值存储在其中。由于栈具有固定大小,因此分配内存和访问数据的速度都非常快。
堆(Heap)是一种非线性数据结构,它用于存储引用类型的值(例如对象、数组、函数等)。当我们声明一个引用类型的变量时,JavaScript 引擎会在堆中为该变量分配一块内存空间,并将变量的地址存储在栈中。由于堆具有动态大小,因此分配内存和访问数据的速度相对较慢。但是,堆可以存储大量数据,并且可以动态增长和收缩。
基本类型:采用的是值传递。
引用类型:则是地址传递。
引用类型的数据的地址指针是存储于栈中的,将存放在栈内存中的地址赋值给接收的变量。当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据(保存在堆内存中,包含引用类型的变量实际上保存的不是变量本身,而是指向该对象的指针)。
二、深拷贝与浅拷贝的理解
现在我们回过头来,再去领会一下深拷贝和浅拷贝的含义,是不是感觉通透了~~一丢丢,还是拿图来理解,首先是浅拷贝, 可以看见对象o和obj指向同一块内存空间,这也就是为什么有些时候我们改变o的值会影响到obj,因为读取的是同一个内存地址呀~~
那么深拷贝不来一个图解总感觉不公平~~
说来就来~~
如图所示,深拷贝其实是在内存中开辟了新的地址,打个比方,这个深拷贝就像你和你邻居家,你家吃饭,你邻居家是不知道的,自然就是不会影响的,
三、常见的深拷贝和浅拷贝
浅拷贝:Object.assign, ...扩展运算符,数组的 slice()
方法进行浅拷贝、使用数组的 concat()
方法进行浅拷贝
深拷贝:JSON.parse(JSON.stringify())
浅拷贝举例
// 使用扩展运算符(`...`)进行浅拷贝:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = { ...obj1 };
// 使用数组的 `slice()` 方法进行浅拷贝:
let arr1 = [1, 2, { a: 3 }];
let arr2 = arr1.slice();
// 使用数组的 `concat()` 方法进行浅拷贝:
let arr1 = [1, 2, { a: 3 }];
let arr2 = arr1.concat();
// 使用objet.assign进行浅拷贝
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = Object.assign({}, obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3
上面的方法都只能进行浅拷贝,它们只复制对象或数组的第一层元素,而不会递归复制它们所包含的子对象。
// 深拷贝
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 2
使用JSON.parse(JSON.stringify(obj))
完成深拷贝,但是这种方法有一定是问题,
例如
1、如果obj
中存在时间对象,那么在使用JSON.parse(JSON.stringify(obj))
之后,时间对象会变成字符串。
2、如果obj
中有RegExp、Error对象,则序列化的结果将只得到空对象。
3、如果obj
中有函数或者undefined,则序列化的结果会丢失函数和undefined。
4、如果对象中存在循环引用的情况,也无法正确实现深拷贝。
看起来这个JSON.parse(JSON.stringify(obj))
也不能乱用,要根据自己的情况而定,所以这里我们还可以通过手写一个深拷贝函数完成深拷贝,只要尽可能的把深拷贝情况思考完整即可。
四、写一个深拷贝函数
这个函数的写法我给你大家分成了两块,一块是基础版本(未对循环引用做处理),一块是进阶版本(处理过的),主要原因是因为,在面试过程中,基本上写到基础班就够了,如果需要深入了解的可以使用进阶班(包含循环引用的)。
基础版本:
function deepCopy(obj) {
let result;
if (typeof obj === 'object') {
// 引用数据类型
// 判断数组
if (Array.isArray(obj)) {
result = [];
for (let i = 0; i < obj.length; i++) {
result[i] = deepCopy(obj[i]);
}
} else if (obj === null) {
// null
result = null;
} else if (obj.constructor === RegExp) {
//正则
result = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Date) {
// 日期
result = new Date(obj);
} else if (obj instanceof Error) {
// 处理error
result = new Error(obj.message);
result.name = obj.name;
result.stack = obj.stack;
} else {
result = {};
for (let key in obj) {
result[key] = deepCopy(obj[key]);
}
}
} else {
// 基础数据类型
result = obj;
}
return result;
}
进阶版本~~~~~~
// 函数中添加一个额外的参数`cache`,用来记录已经拷贝过的对象。
//这样,在遇到循环引用时,函数可以直接返回已经拷贝过的对象,而不会陷入死循环
function deepCopy(obj, cache = new WeakMap()) {
let result;
if (typeof obj === 'object') {
if (cache.has(obj)) {
return cache.get(obj);
}
if (Array.isArray(obj)) {
result = [];
cache.set(obj, result);
for (let i = 0; i < obj.length; i++) {
result[i] = deepCopy(obj[i], cache);
}
} else if (obj === null) {
result = null;
} else if (obj.constructor === RegExp) {
result = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Date) {
result = new Date(obj);
} else if (obj instanceof Error) {
result = new Error(obj.message);
result.name = obj.name;
result.stack = obj.stack;
} else {
result = {};
cache.set(obj, result);
for (let key in obj) {
result[key] = deepCopy(obj[key], cache);
}
}
} else {
result = obj;
}
return result;
}
这个函数在每次遇到一个新的对象时,都会先检查这个对象是否已经被拷贝过。如果已经被拷贝过,那么直接返回已经拷贝过的对象;否则,将这个对象添加到cache
中,并继续进行深拷贝。
enm~差不多就这些吧,希望各位老师不吝赐教~,共同进步