欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

深拷贝和浅拷贝

程序员文章站 2022-04-08 17:53:35
...

引言

看下面这个问题

function changeStuff(a, b, c)
{
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);
console.log(obj1.item);    
console.log(obj2.item);

想想打印出来的值分别是什么

正确答案是 10changedunchanged

为什么会这样,这就要说到数据类型的值的存储方式了,这里要区别于数据类型
比如 string,number,boolean,object 等是数据类型。
而 “hello”,1,false,{} 这些属于数据类型的值。

在 js 中,基本数据类型的值是存放在栈中的,而引用类型的值是存放在堆中的,但是对这个对象的引用是放在栈中的。

怎么理解呢,比如有一个人叫张三,但是李四喜欢叫他小张,王五喜欢叫他老张。这里小张和老张就是对张三的引用,它们只是一个代指,而真正的值是张三本人,这就类似于同一个人可以有不同的绰号。

我们再看回这道题其中,num 是基本数据类型,obj1 和 obj2 是对象,属于引用类型。
在把参数传进函数时执行了如下的操作

var a = num;
var b = obj1;
var c = obj2; 

所以,修改 a 的时候不会对 num 造成影响,因为他们是两个不同的变量。

而 b 和 c 则是分别指向了 obj1 和 obj2 真正引用的对象。这就像原来张三叫 obj1,现在 b 指的也是张三本人,只不过有人把张三叫做 b。
执行 b.item 和 obj1.item 是同样的操作。

但是 c = {item: “changed”} 是让 c 指向了一个新的对象,和原来的 obj2 的关系已经断了。

这就相当于一开始 obj2 指的是张三,然后 c = obj2,即 c 指的也是张三。这时候 c = {item: “changed”} 相对于 c = new Object();即 c 现在指向李四了,和张三没有任何关系了。所以对李四的任何操作都不会影响张三。

1. 什么是深拷贝和浅拷贝

弄清楚了数据的值的存储方式我们来解释一下什么叫深拷贝和浅拷贝,看下面一段代码

// 假设有下面一个对象
var obj = {
    num:1,
    array:[1,2,3]
}

现在让你创建一个新对象 objNew,objNew 和 obj 的属性和值都是相同的,你会怎么做。

// 刚刚讲过 objNew = obj 是不行的,因为这样它们指的是同一个对象,所以我们需要创建一个新对象
var objNew = new Object();  // 或 objNew = {}
objNew.num = obj.num;
objNew.array = obj.array;

这样就行了吗,我们试着操作一下

objNew.num = 3;
console.log(obj.num); // 1
console.log(objNew.num); // 3 果然不一样

objNew.array.push(4);
console.log(obj.array); // [1,2,3,4]
console.log(objNew.array); // [1,2,3,4]

为什么对 objNew 的 array 进行操作会影响 obj 呢。同样回到之前的问题,因为 obj.array 和 objNew.array 指向的是同一块内存区域,即同一个引用类型的值,所以对一方进行操作就会影响另一方。

那么到底什么是浅拷贝什么是深拷贝呢?

像上面这种只能复制基本类型的值,不能复制引用类型的值就叫做浅拷贝,浅拷贝的基本类型的值是各自独立的,而引用类型的值还是和之前的对象一样是公用的。

深拷贝即创建了一个新的对象,这个对象中基本类型值是独立的,引用类型值也是独立的,修改这个对象的引用类型的值不会影响原对象的值。

2. 如何实现浅拷贝

那么如何实现浅拷贝呢,刚刚那种方法对于不同的对象没有适用性,有哪些可以实现浅拷贝的方法呢

2.1 Object.assign()

ES6 新增了一个 Object.assign 的方法,这个方法接收多个参数,第一个参数是目标对象,后面的参数是源对象,用法如下

var target  = {}; // 目标对象
var source = {a : 1}; // 源对象
Object.assign(target,source);
// target = {a : 1};

有人把 Object.assign() 说成一种可以对非嵌套对象进行深拷贝的方法,其实这是不对的,因为这种非嵌套对象的深拷贝和浅拷贝是没有区别的,本质上还是浅拷贝。

2.2 自定义函数

我们可以自己写一个简单的函数来实现浅拷贝,思路是对于对象中的每个值,让新对象的值等于原对象的值

function shallowCopy(objNew,obj){ // 目标对象和原对象
    var objNew = objNew||{};
    for(let i in obj){
        objNew[i] = obj[i];
    }
    return objNew;
}

这样就实现了一个最简单的浅拷贝

3. 如何实现深拷贝

实现深拷贝的主要思想就是当属性是对象或者数组的时候,就创建新的对象或者数组,递归,直到出现基本值,将基本值复制到对象或者数组当中,最后返回创建的这个新的对象或者数组。

3.1 转成 JSON 再转回来

首先我们要理解 JSON.stringify() 是将一个对象序列化为 JSON 的字符串。即把对象变成一个字符串,如

var obj = {
    num:1,
    array:[1,2,3]
}
var str = JSON.stringify(obj);
console.log(str); // "{"num":1,"array":[1,2,3]}"

然后 JSON.parse() 将一个 JSON 字符串解析为原生 js 对象。即把字符串转换为对象,如

var objNew = JSON.parse(str);
console.log(objNew); // {num: 1, array: Array(3)}

因此,objNew 与原对象没有任何关系,这样就实现了深拷贝。

3.2 自定义函数

我们也可以根据自己的思路自定义一个深拷贝的函数,如下

function deepCopy(opt,obj){ // 参数分别为目标对象和原对象
    var objNew = opt||{};
    for(let i in obj){
        // 如果当前属性是对象
        if(typeof i === 'Object'){
            if(obj[i].constructor === Array){
                objNew[i] = [];
            }else if(obj[i].constructor === Object){
                objNew[i] = {};
            }
            deepCopy(objNew[i],obj[i]); 
        }else{ // 否则直接相等
            objNew[i] = obj[i];
        }
    }
    return objNew;
}

总结

深拷贝主要用在一些不希望改变原对象的情况下复用对象,比如后台传过来一个 json 对象,自己测试的时候不希望改变这个对象,可以拷贝一个,对拷贝的对象进行操作。

浅拷贝主要复制的是对象的基本类型的值,而引用类型值和原对象还是共用一个,所以修改新对象的引用类型值会影响原对象,这往往是我们不想要的,所以需要注意。

相关标签: 深拷贝 浅拷贝