对Javscript中浅拷贝和深拷贝的探索和详解

Posted 再见已是路人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对Javscript中浅拷贝和深拷贝的探索和详解相关的知识,希望对你有一定的参考价值。

前言

要想对深浅拷贝理解透彻,那我们还真不能开门见山,我们还得先了解一下门,再找到钥匙,把门打开后才会看到山,这样一来你也就完全理解了山存在的意义;

因此我们要先了解一下中Js的数据类型以及存储方式,就会再认识到什么是栈(stack)和堆(heap),接下来你还会意识到对象的赋值和赋址的区别,然后会继续探索对深浅拷贝的理解以及赋值和浅拷贝的区别,最后我们还要能手动实现对象的深浅拷贝

事实上,深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的

那接下来我们也会围绕以上提到的几个知识点串联一下,融会贯通,一步一步去探索和理解深浅拷贝;


Js数据类型和存储

面试中第一个最常问到的就是让我们说出Js有哪些数据类型了,这个大家都不陌生,有两种数据类型,分别是基本数据类型引用数据类型

基本数据类型

  • 基本数据类型包括 String,Number,Boolean,Undefined,Null,Symbol(其中Symbol是ES6新增,表示独一无二的值)
  • 数据类型存储在栈(stack)内存中的,数据大小确定,内存空间大小可以分配。

引用数据类型

  • 引用类型统称为Object,细分的话,分为5个:Object对象、Array数组、RegExp正则、Date时间对象、Function函数
  • 引用数据类型是存储在堆(heap)内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置
  • 每个内存空间大小不一样,要根据情况开进行特定的分配

那什么是栈内存什么是堆内存,这两种数据结构又是以什么的形式去存储的呢,那接下来我们就一起去了解一下栈和堆;


栈(stack)和堆(heap)

在编译阶段,除了声明变量和函数,查找环境中的标识符这两项工作之外,还会进行内存分配。不同类型的数据会分配到不同的内存空间;

栈内存
关于栈,你可以结合这么一个贴切的例子来理解,一条单车道的单行线,一端被堵住了,而另一端入口处没有任何提示信息,堵住之后就只能后进去的车子先出来,这时这个堵住的单行线就可以被看作是一个栈容器,车子开进单行线的操作叫做入栈,车子倒出去的操作叫做出栈。

所以,栈就是类似于一端被堵住的单行线,车子类似于栈中的元素,栈中的元素满足后进先出的特点。(前面我还写了一篇JavaScript的执行机制——调用栈,可供参考有助于更理解什么是栈)

  • 栈主要用于来保存基本值和引用类型值的地址;
  • 存放的变量一般都是已知大小或者已知上限范围的,算是一种简单存储;
  • 栈是自动分配的相对固定大小的内存空间,其数据读取快,写入速度快,但存储内容少,变量一旦不使用就由系统自动清理释放;

堆内存

堆的存取是随意,这就如同我们在图书馆的书架上取书, 虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书, 我们只需要关心书的名字。

  • 用来保存一组无序且唯一的引用类型值,可以使用栈中的键名来取得。
  • 堆的读取和写入速度慢,但存储的内容多,一般对象会存储在堆中,存储的数据对于大小在这方面都是未知的
  • 堆是动态分配的内存,大小不定也不会自动释放。


赋值和赋址的区别

前面之所以要说明什么是内存中的堆、栈以及变量类型,实际上就是为了更好的理解赋值,深拷贝以及浅拷贝;

基本类型与引用类型最大的区别实际就是赋值与赋址的区别。

赋值与赋址

为一个变量赋基本值时,实际上是创建一个新值,然后把该值赋给新变量,可以说这是一种真正意义上的" 赋值 “;

为一个变量赋引用值时,实际上是为新变量添加一个指针,指向堆内存中的一个对象,属于一种” 赋址 "操作;

  var a = [1,2,3,4,5];
  var b = a;//赋址 ,把a的地址指针赋值给了b变量
  var c = a[0];//赋值,把对象中的属性/数组中的数组项赋值给变量,这时变量C是基本数据类型,存储在栈内存中
  alert(b);//1,2,3,4,5
  alert(c);//1

  //改变数值        
  b[4] = 6;
  c = 7;
  alert(a[4]);//6
  alert(a[0]);//1

从上面我们可以得知,当我改变b中的数据时,a中数据也发生了变化;但是当我改变c的数据值时,a却没有发生改变。

这就是赋值与赋址的区别,因为a是数组,属于引用类型,所以它赋予给b的时候传的是栈中的地址(相当于新建了一个不同名“指针”),而不是堆内存中的对象。而c仅仅是从a堆内存中获取的一个数据值,并保存在栈中。所以b修改的时候,会根据地址回到a堆中修改,c则直接在栈中修改,并且不能指向a堆内存中。


浅拷贝和深拷贝

所谓拷贝,就是赋值。把一个变量赋给另外一个变量,就是把变量的内容进行拷贝。把一个对象的值赋给另外一个对象,就是把一个对象拷贝一份。

基本类型数据赋值时,赋的是数据,所以不存在深拷贝和浅拷贝的问题;

浅拷贝
创建一个新的对象,不会指向同一个地址,这个对象有着原始对象属性值的精确拷贝。就是对象的浅拷贝只会对“主”对象进行拷贝,拷贝的是对象属性的基本类型的值,如果属性是引用类型,拷贝的就是内存地址,拷贝的不深,所以称为浅拷贝;

  • 修改浅拷贝对象第一层的非对象引用类属性,都不会影响原对象
  • 由于浅拷贝不会拷贝对象里面的对象,“里面的对象”会和原对象共享内存,所以修改浅拷贝对象的子属性对象里面的属性,原对象也会受到影响

利用 for…in循环实现浅拷贝

	let obj = {
	    name: '李四',
	    age: 20,
	    action: {
	        eat: '苹果',
	        sing: '《安静》'
	    },
	};
	
	let obj2 = {};
	for (let key in obj) {
	    obj2[key] = obj[key];
	}
	
	obj2.name = '小六';
	obj2.sex = '男';
	obj2.sction.eat = '草莓';
	
	console.log('obj', obj);
	// obj  {name: '李四',age: 20,action: {eat: '草莓',sing: "《安静》"}}
	console.log('obj2', obj2);
	// obj2  {name: '小六',age: 20,action: {eat: '草莓',sing: "《安静》"},sex:‘男’}

利用Object.assign()方法

let obj = {
	    name: '李四',
	    age: 20,
	    action: {
	        eat: '苹果',
	        sing: '《安静》'
	    },
	};
	
    Object.assign(obj2, obj);
	
	obj2.name = '张三';
	obj2.sex = '男';
	obj2.sction.eat = '草莓';
	
	console.log('obj', obj);
	// obj  {name: '李四',age: 20,action: {eat: '草莓',sing: "《安静》"}}
	console.log('obj2', obj2);
	// obj2  {name: '张三',age: 20,action: {eat: '草莓',sing: "《安静》"},sex:‘男’}

深拷贝

深拷贝不仅将元对象的各个属性逐个复制,还将原对象各个属性所包含的对象属性也一次采用深复制的方法递归复制到新对象上,所以修改拷贝后的对象不会影响原对象;

使用递归的方式实现深拷贝

   function deepClone(obj){
          // 基本类型,直接赋值
          if( typeof obj != 'object' ){
              return  new_obj = obj
          }
          //引用类型 判断拷贝类型是数组还是对象
          var new_obj = Array.isArray(obj) ? [] : {};
           //数组
          if(obj instanceof Array ){
              for(i = 0; i < obj.length; i++ ){
                  new_obj[i] = obj[i];
                  if(typeof new_obj[i] == 'object'){
                      deepClone(new_obj[i])
                  }
              }
          }else{ //对象
              for (let key in obj) {
                  if (obj.hasOwnProperty(key)) {
                      // 对象中的数组和对象
                      if (typeof obj[key] == 'object') {
                          new_obj[key] = deepClone(obj[key]); 
                      }else{//对象中没有引用类型
                          new_obj[key] = obj[key]
                      }  
                  }
              }
          }
          return new_obj;
      }

通过 JSON 对象实现深拷贝

  • 这种方式无法拷贝 正则表达式,undefine,function
 //通过js的内置对象JSON来进行数组对象的深拷贝
    function deepClone2(obj) {
      var _obj = JSON.stringify(obj),
        objClone = JSON.parse(_obj);
      return objClone;
    }

通过jQuery的extend方法实现深拷贝

  • 第一个参数 true 为深拷贝,false 为浅拷贝
  var newArray = $.extend(true,[],array);

slice()、concat()对数组进行深拷贝

	let arr = [1, 2, 3, 4, 5, 6];
	
	let arr2 = arr.slice(0);
	arr2[0] = 'Rose';
	arr2[1] = '女';
	
	let arr3 = arr.concat();
	arr3[0] = 'Tom';
	arr3[1] = '男';
	
	console.log('arr', arr); // [1, 2, 3, 4, 5, 6]
	console.log('arr2', arr2); // ["Rose", "女", 3, 4, 5, 6]
	console.log('arr3', arr3); // ["Tom", "男", 3, 4, 5, 6]

赋值、浅拷贝和深拷贝的区别

和原数据是否指向同一个对象第一层数据为基本数据类型原数据中包含的子对象
赋值会使原数据一起改变会使原数据一起改变
浅拷贝不会使原数据一起改变会使原数据一起改变
深拷贝不会使原数据一起改变不会使原数据一起改变

上面已经详细的介绍了赋值,浅拷贝和深拷贝,不理解的可以再认真查看,为了更方便了解,下面我放上三者的内存图:

赋值

浅拷贝

深拷贝

上面的内存图是目前我按照个人的理解去画的,如您参考时发现错误,请及时留言纠正,我们一起学习,一起进步;

以上是关于对Javscript中浅拷贝和深拷贝的探索和详解的主要内容,如果未能解决你的问题,请参考以下文章

Java中浅拷贝和深拷贝

Java中浅拷贝和深拷贝

初始JAVA中浅拷贝和深拷贝

彻底理解Python中浅拷贝和深拷贝的区别

js中浅拷贝和深拷贝以及深拷贝的实现

Python中浅拷贝与深拷贝的案例实践