【js进阶】-深浅拷贝

一、为什么会出现深浅拷贝

实质上是由于JS对基本类型和引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象,而JS不允许我们直接操作内存中的地址,也就是不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。

二、js中复制初体验

当我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西(也就是值)并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,改变其中一个对象,会影响到另外一个。

var num = 10;
var num2 = num;  
var obj = {
    name: 'Nicholas'
}
var obj2 = obj;

obj.name = 'Lee';
obj2.name; // 'Lee'

在这里插入图片描述

解析:var num2 = num; 属于基本类型的复制,直接在栈内存中创建了一块新内存空间给num2,存的值同样是10,num2和num完全无关,而var obj2 = obj因为obj是一个对象,所以属于引用类型的复制,所以此时复制给obj2的只是原先保存在obj变量中的引用地址(指针)而已,此操作过后,obj和obj2两个变量存的都是堆内存那个实际对象的引用地址,两个变量指向了同一个内存空间,所以当obj修改对象的name属性时,其实改的是堆内存中的那个对象,由于obj2和obj指向的是同一个对象,所以打印obj2.name就是修改后的那个name值了

三、js中的深浅拷贝(外加首层浅拷贝)

注意:我们所说的深浅拷贝一般用于引用类型的复制,不用于基本类型复制

浅拷贝:

只复制了引用而未真正复制值

几种浅拷贝的方式

1、“ = ”运算符

const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

const cloneArray = originArray;
const cloneObj = originObj;

console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}

cloneArray.push(6);
cloneObj.a = {aa:'aa'};

console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]

console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}

上面的代码是最简单的利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArray 和 cloneObj 改变,originArray 和 originObj 也随着发生了变化。

深拷贝:

深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了,只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。

实现深拷贝的方式

1、利用 JSON 对象中的 parse 和 stringify

---------------------------数组的深拷贝-----------------------------
const arr1 =[1,2,3,4,5]
const arr2 =JSON.parse(JSON.stringify(arr1))   //深拷贝
console.log(arr2)   //[1,2,3,4,5]
arr1.push(6)
console.log(arr1) //[1,2,3,4,5,6]   //添加了6
console.log(arr2) //[1,2,3,4,5]  还是原先的值,两者不相关

---------------------------对象的深拷贝-----------------------------
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

以上情况确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。比如下面这样的一个对象就不适用:

const originObj = {
  name:'haha',
  sayHello:function(){
    console.log('Hello haha');
  }
}
console.log(originObj); // {name: "haha", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "haha"}

发现在 cloneObj 中,有属性丢失了。。。那是为什么呢?

结论:因为使用JSON.parse/stringify在遇到函数、undefined、Symbol、正则等时会丢失,
new Date会被转成时间字符串形式,无法对上述几种情况进行正常复制,所以当遇到要复制的对象中包含函数的时候,就不能使用JSON.parse/stringify进行深拷贝了

2、利用递归来实现每一层都重新创建对象并赋值

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in source){ // 遍历目标
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

好的,我们来试试

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

ok。那再试试带有函数的:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}

也ok

首层浅拷贝:

这里相信有好多同学都是第一次听到这个词汇,我们来解释下到底什么是首层浅拷贝,其实就是对目标对象的第一层进行深拷贝,然后后面的是浅拷贝,这就称作“首层浅拷贝”。

实现首层浅拷贝的方式

1、数组的concat()方法

-------------------简单情况都是基本类型时------------------
const originArray = [1,2,3,4,5];  //一层数组
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];

----------------------------有引用类型时---------------------------------
const originArray = [1,[1,2,3],{a:1}];   //多层数组
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[0]=2;
console.log(cloneArray)  // [2,[1,2,3],{a:1}];   
console.log(originArray)  // [1,[1,2,3],{a:1}];   修改cloneArray的第一个值不影响原数组
cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]   //修改cloneArray的数组和对象值时会影响原数组,说明两者的引用是同一个

结论:concat 只是对数组的第一层进行深拷贝。

2、slice

-------------------简单情况都是基本类型时------------------
const originArray = [1,2,3,4,5];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];  //两者互不干扰,是深拷贝

-------------------有引用类型时------------------
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray[0] =2;  //修改克隆数组的第一个值
console.log(cloneArray)  // [2,[1,2,3],{a:1}];   
console.log(originArray)  // [1,[1,2,3],{a:1}];   修改cloneArray的第一个值不影响原数组
cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]  //修改cloneArray的对象和数组则会影响原数组,说明引用的是同一个对象和数组

结论:slice 只是对数组的第一层进行深拷贝。

3、Object.assign()

-------------------简单情况都是基本类型时------------------
let srcObj = {'name': 'lilei', 'age': '20'};
let copyObj2 = Object.assign({}, srcObj);
console.log('srcObj', srcObj);		//'name': 'lilei', 'age': '20'
console.log('copyObj2', copyObj2);	//'name': 'lilei', 'age': '20'
srcObj.name="zhangsan";
console.log('srcObj', srcObj);		//'name': 'zhangsan', 'age': '20'
console.log('copyObj2', copyObj2);	//'name': 'lilei', 'age': '20'
copyObj2.age="10";
console.log('srcObj', srcObj);		//'name': 'zhangsan', 'age': '20'
console.log('copyObj2', copyObj2);	//'name': 'lilei', 'age': '10'

---------------------------有引用类型时----------------------------
let srcObj = {'name': 'lilei', 'grade': {'chi':"80", 'eng':"100"}};
let copyObj2 = Object.assign({}, srcObj);
copyObj2.name="zhangsan";
copyObj2.grade.chi="50";
console.log('srcObj', srcObj);		//name: "lisi" grade: {chi: "50", eng: "100"}
console.log('copyObj2', copyObj2);	//name: "zhangsan" grade: {chi: "50", eng: "100"}

结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

4、… 展开运算符

const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};

const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]

const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}

结论:… 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。

四、总结

  1. 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
  2. JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
  3. JSON.stringify 实现的是深拷贝,但是对目标对象有要求;
  4. 若想真正意义上的深拷贝,请递归。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ronychen’s blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值