javascript中的深拷贝与浅拷贝,常见方法以及优缺点,手动实现深拷贝函数

485 阅读6分钟

深浅拷贝可谓是面试中高频考点,那么本期就详细讲一讲这个令人头疼的问题~本文涉及数据结构的讲解,深浅拷贝的区别,举例,算法,以及优缺点。文章较长,如果哪里你会,可以跳着看,汲取自己需要的即可~~

一、数据结构中的堆和栈

老规矩---先撸定义:

深拷贝实际上是在内存中开辟一块新的地址来存放复制的对象。(简单来说就是两块地方,谁都不影响谁)

浅拷贝实际上是 只拷贝一层 更深层对象级别只拷贝地址。(指针指向同一个内存地址)

接下来咱们还是先普及数据结构,以防止小白同学一头雾水,我比较懒没画图,在网上找的图,拿来用一下,给大家讲解。

首先,js中数据类型分为基本数据和引用数据类型。 基本数据类型(Undefined,String,Boolean,Null,Number)都是存放在栈中,它遵循后进先出(LIFO)的原则。在 JavaScript 中,当我们声明一个基本类型的变量时,JavaScript 引擎会在栈中为该变量分配一块内存空间,并将变量的值存储在其中。由于栈具有固定大小,因此分配内存和访问数据的速度都非常快。

堆(Heap)是一种非线性数据结构,它用于存储引用类型的值(例如对象、数组、函数等)。当我们声明一个引用类型的变量时,JavaScript 引擎会在堆中为该变量分配一块内存空间,并将变量的地址存储在栈中。由于堆具有动态大小,因此分配内存和访问数据的速度相对较慢。但是,堆可以存储大量数据,并且可以动态增长和收缩。

image.png

基本类型:采用的是值传递

引用类型:则是地址传递

引用类型的数据的地址指针是存储于栈中的,将存放在栈内存中的地址赋值给接收的变量。当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据(保存在堆内存中,包含引用类型的变量实际上保存的不是变量本身,而是指向该对象的指针)。

二、深拷贝与浅拷贝的理解

现在我们回过头来,再去领会一下深拷贝和浅拷贝的含义,是不是感觉通透了~~一丢丢,还是拿图来理解,首先是浅拷贝, 可以看见对象o和obj指向同一块内存空间,这也就是为什么有些时候我们改变o的值会影响到obj,因为读取的是同一个内存地址呀~~

image.png 那么深拷贝不来一个图解总感觉不公平~~ 说来就来~~ 如图所示,深拷贝其实是在内存中开辟了新的地址,打个比方,这个深拷贝就像你和你邻居家,你家吃饭,你邻居家是不知道的,自然就是不会影响的, image.png

三、常见的深拷贝和浅拷贝

浅拷贝: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~差不多就这些吧,希望各位老师不吝赐教~,共同进步