浅拷贝与深拷贝的细节(js方向)

值类型和引用数据类型在栈和堆中的细节区别:

在JavaScript中,存在一个浅拷贝与深拷贝的问题,我们都知道基础数据类型它是在栈上开辟一个地址存储一个变量,给这个变量所赋的值同样是保存在栈中的,这里重点是看值的问题,每次新定义一个变量都会在栈上重新开辟一个地址空间,保存相对应赋的值,因此,不同变量在栈上给彼此赋值的操作就是复制值的操作;我们可以看如下的一段代码:

var a = 100;//在栈上开辟一个地址空间,定义了a变量并赋值100
var b = a;//在栈上另外开辟一个地址空间,定义了b变量并将a的值赋值给b
a = 50;//给a重新赋值为50
console.log(b);//100 //在刚开始定义b变量的时候就已经将a=100的值赋值给了
//b自己,从那一刻开始,b自身便是100的值,及时后面a又重新赋值,使a的值发生了
//改变,但a的重新赋值是在定义b之后进行的,因此a此时的改变并不会影响到b之前
//已经定义好的变量值,所以在这里b输出的即为100

而引用数据类型则不同,引用数据类型的值是在堆上的,而且是根据对应的堆上的地址去判断该引用数据类型的被指向问题的,我们可以看如下示例代码:

var arr = [1,2,3,4];//在栈上定义一个变量arr,到对应的堆中申请一个地址空间填入数组[1,2,3,4],
//此时arr变量装的就是数组[1,2,3,4]所存在的一个地址
var brr = arr;//这里将arr赋值给brr,前面我们讲过,在栈中只存在简单的值复制,对于引用数据类型
//来讲,是获取了变量被赋值内容在堆中的地址值,发生的不是栈上的值复制,而是堆中的地址值复制,
//所以这里必然是将对应的堆中的地址值复制给了brr,所以brr拥有了和arr一样的地址值,此时brr和arr
//指向堆中同一个地址空间,因此brr和arr实际上是同一个东西
console.log(brr==arr);//true
var newArr = brr.push(6);
console.log(newArr)//返回新数组的长度5
console.log(brr,arr);//[1,2,3,4,6] [1,2,3,4,6]

摆脱原引用数据类型的束缚,自立门户拷贝原引用数据类型的所有值(深克隆核心思想):

现在我们来实现一个对象拷贝,此时我们的要求是要让新拷贝的对象和被拷贝的对象彻底脱离开关系,前面我们在引用数据类型的例子中已经知道,引用数据类型重要的是对地址值的赋值拷贝的相关特性,通俗来讲就是引用数据型要判断它与另一个引用数据类型的关系不能通过变量名=后面的内容去判断而是通过该引用数据类型在堆中对应的地址空间去判断,以下是两个无关系互相独立的对象拷贝示例:

//在栈中定义一个obj变量,对应对象内容指向并存储在堆中的新开辟的一个地址空间
var obj = {
    name:'老张',
    age:18,
    hobby:'打豆豆'
}
//在栈中定义一个obj1变量,也对应的指向堆中一个新开辟的地址空间
var obj1 = {}
//以下进行对象拷贝
//将obj对象进行遍历,将他的属性名和对应的属性名依次遍历
for(var key in obj){
    //obj[key]在这里面表示的是obj中每个属性对应的属性值,将它赋值给obj1对象中对应的属性,
    //完成属性赋值操作,赋值完成后,obj1对象中也就拥有了与obj同样的属性和对应属性值
    obj1[key] = obj[key]
}
obj.name = '小白'
console.log(obj1.name)//老张
console.log(obj.name)//小白
//由此可以看出,引用数据类型之间的关系藕带为堆中的地址值

接下来我们拷贝一下数组,要求和对象一样,将原本的数组与拷贝成功的数组独立开来,两者并无什么直接关联,依然还是从地址堆中的地址值入手,因此思路已然建立,我们需要准备一个数组,再另外在堆空间中开辟一个新的地址,也就是说这两个数组对象不是一个数组对象,代码如下:

//在栈中定义一个变量,该变量指向堆中一个对应的地址空间并存储相对应的数组内容
var arr = [5,8,4,'你好'];
//在栈中再次定义一个变量,该变量指向堆中另外的新的地址空间,并存储对应内容
var arr1 = [];
//以下是拷贝过程
//面对数组,我们使用普通的for循环得到其中的目标元素
for(var i = 0;i<arr.length;i++){
    arr1[i] = arr[i] //将arr当下遍历的索引值对应的元素内容赋值给对应的arr1对应索引值的元素内容
    //当然我们也可以通过给数组arr1添加元素的方式进行依次元素的遍历添加
    //arr1.push(arr[i]);//这一句的效果也是一样的,只是使用了数组的push方法
}
arr.push('小白');
console.log(arr1)//[5,8,4,'你好']
console.log(arr1 == arr)//false
console.log(arr1,arr)//[5,8,4,'你好'] [5,8,4,'你好','小白']

引用数据类型嵌套关系的拷贝现象及浅谈深克隆思想:

以上我们拷贝的都是值类型或者引用类型的第一层的值,所以不论是浅拷贝还是深拷贝,我们都无法很直观的从表面上看出来区别,只能通过地址的比较对具有拷贝关系的两个对象进行关系的判断,但当我们遇到引用数据类型的拷贝时就可能会面临第二层值的问题,以下是一个示例:

var arr = [1,2,'你是谁',[3,5]];//像这种情况,数组arr中就包含了另外一个数组[3,5],这个时候
//如果进行的是浅拷贝就会出现问题,针对arr中的数组进行值的更改就会影响到拷贝的数组内容跟arr
//发生一样的变化
var arr1 = [...arr];//此处使用扩展运算符展开arr的数组内容装在新定义且在堆上开辟了新的内存
//空间的数组arr1中,所以此时arr1肯定是!=arr的,但是赋值过来的arr数组中的数组在堆中的地址是
//一样的证明扩展运算符是实现浅拷贝的方式之一
console.log(arr1 == arr);//false
console.log(arr[3] == arr1[3]);//true
//以上两条输出语句足以证明浅拷贝只拷贝第一层的值,第二层拷贝的是地址,如果我们不想让引用数据
//类型中的引用数据类型拷贝的是地址,也就是不希望引用数据类型之间因为地址值的问题相互牵制的话,
//我们就需要像在第一层的时候就建立一个新的对象去接复制过来的值,这样才能改变它的地址指向,使
//两者不再是同一个东西,从而互不影响,这也就是深克隆的思想
arr[3].push(7);//在arr数组中的下标为3的元素也就是[3,5]这个数组内添加一个元素7
console.log(arr);//[1,2,'你是谁',[3,5,7]]
console.log(arr1);//打印arr1得到的数组和arr得到的最新数组结果是一样的



实现深克隆(递归思想):

当我们碰到的数据类型为引用数据类型且内部嵌套了多层引用数据类型的话,我们希望把该引用数据类型整个内容都克隆过来,但是如果只是单纯的在刚开始定义一个新的引用数据类型去接复制过来的值,就没办法克服克隆后出现的内部引用数据类型受原引用数据的牵制的缺陷,所以这里很明显我们需要不断地在找到的引用数据类型中发现新的引用数据类型,并且就当前发现的引用数据类型进行深克隆行为,也就是创建一个新的引用数据类型,将当前引用数据类型再拷贝到新的引用数据类型中,反复以往,直到最后无法找到新的引用数据类型就结束该行为,这就是深克隆,下面我们来看一段代码:

//方式一 我们可以在进行深克隆之前就将新的要拷贝的数组准备好,也就是下面的newObj空对象
var obj = {
    name:'rose',
    age:23,
    hobby:'打豆豆',
    room:[203,406,211],
    daily:{
        action:'work',
        entertainment:'watch movie',
        sport:'play basketball'
    }
};
var newObj = {};
//然后基于以上的条件我们封装一个深克隆过程的方法
//这个方法需要传递两个参数,一个是需要被克隆的对象,一个是克隆得到的目标对象,也就是新对象
function deepClone(newO,old){
    //首先先给传入的需要被克隆的对象进行for遍历,我么这里采用for in进行
    for(var key in old){
        //判断遍历到的对象的属性值或者数组当前的下标是什么类型
        //使用old[key] instanceof Array 和old[key] instanceof Object进行判断
        if(old[key] instanceof Array){//如果这个值返回的是true,就证明是Array的引用数据类型
            //这个时候给传入的参数newO[key]赋值一个空数组
            newO[key] = [];//这里就相当于对属性值赋值给新对象的操作
            deepClone(newO[key],old[key]);//上面这两句的意思就是循环到属性值为引用数据
            //类型时,先给对应的新对象newO里面对应位置的属性赋值,使其拥有当前拷贝对象当前遍历
            //的属性,再赋值为一个新地址对应的与原拷贝对象毫不相关的对象,完成这个步骤之后又进
            //行deepClone的调用,再重新判断当前的old[key]是否是引用数据类型,不是则直接赋值
            //完成拷贝
        }else if(old[key] instanceof Object){//如果这个值返回的是true,就证明是Object的引
            //用数据类型
            //这个时候给传入的参数newO[key]赋值一个空对象
            newO[key] = {};
            deepClone(newO[key],old[key]);
        }else{//如果既不是Array类型,也不是Object类型,是普通的值类型的话就直接把获取到的
              //属性值赋值给新对象
            newO[key] = old[key]
        }
    }
    return newO;//功能函数结束返回newO对象
};
deepClone(newObj,obj)
//上述的这种方法有一定的方法顺序规定,我们一定是先对数组进行类型检测,因为如果是对象先检测,
//那无论是数组还是对象都会进入当下的条件内执行函数,数组是基于对象的,范围比对象小,所以要
//先从小范围的开始解决问题,如果是大范围的条件判断就判断不了具体的对象类型了举一个小示例你
//就能明白:
var arr = [];
console.log(arr instanceof Object);//true
console.log(arr instanceof Array);//true


//方式二 我们只需要在开始的时候准备一个需要被克隆的对象,针对该对象到对应的功能函数中进行传参运行
//即可
//定义一个对象
var obj = {
    name:'rose',
    age:23,
    hobby:'打豆豆',
    room:[203,406,211],
    daily:{
        action:'work',
        entertainment:'watch movie',
        sport:'play basketball'
    }  
};
//封装一个深克隆的方法 参数为被克隆的目标对象,返回一个被克隆的对象
function deepClone(obj){
      //传入obj,判断当前obj是不是object类型,obj不能为null
    if(!(typeof obj == 'object' && obj)){//如果传入的obj是否为一个对象类型,如果不是
    //就直接返回这个obj
        return obj
    }
    //定义一个变量,如果当前传入的obj是一个数组那就给他赋值一个新的数组,如果是对象,那就
    //给它赋值一个新的对象
    var copyObj = obj instanceof Array ? [] : {};
    //接下来循环遍历传入的这个对象
    for(var key in obj){
        copyObj[key] = obj[key];//将遍历到的属性值依次赋值给需要拷贝的目标对象中,使克隆对象
        //中也拥有和原对象同样的属性和属性值
        deepClone(obj[key]);//如果当前遍历到的属性值是对象类型,就针对这个获取到的属性值对象再
        //进行一次深克隆操作
    }
    return copyObj;
};
var copyObj1 = deepClone(obj);//定义一个变量接深度克隆后返回的值
console.log(copyObj1,obj);//{name: 'rose', age: 23, hobby: '打豆豆', room: Array(3), daily: {…}}    {name: 'rose', age: 23, hobby: '打豆豆', room: Array(3), daily: {…}}
console.log(copyObj1 == obj)//false

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值