摘要:从而也引出了所谓的深浅复制问题。附注对于浅复制,其实还有其他的实现方式,比如数组中和方法,对于这些还是希望大家自己了解,本本主要针对深浅复制的实现原理进行解析。
前言
在之前写继承的过程谈到了深浅复制的问题,因为有读者反映到需要解析,趁今天周末写一篇解析,今天的主体相对之前来说理解难度低一些,篇幅可能也比较短,诸君按需阅读即可。
从两种数据类型说起在js中,变量的类型可以大致分成两种:基本数据类型和引用数据类型,其中基本数据类型指的是简单的数据段,包括:
Undefined
Null
Boolean
Number
String(字符串在一些其他语言中是被当做对象使用的,属于引用类型,但在js里是基本类型)
而引用类型的值指的是可能包含多个值的对象。可能上面这种描述大家都看过不少,但是有没有思考过为什么要把数据类型这样分呢?本质上,是因为基本数据类型保存在栈内存,而引用类型保存在堆内存中。那再进一步问:为什么要分两种保存方式呢? 根本原因在于保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,但是我们可以把它的地址写在占内存中以供我们访问。举个例子:
var a = 1;//定义了一个number类型 var obj1 = {//定义了一个objr类型 name:"obj" };
在执行这段代码后,内存空间里是这样的:
因为这种保存方式的存在,所以我们在操作变量的时候,如果是基本数据类型,则按值访问,操作的就是变量保存的值;如果是引用类型的值,我们只是通过保存在变量中的引用类型的地址类操作实际对象。从而也引出了所谓的深浅复制问题。
紧接着上文的内容,假设有以下代码:
//例子1 var a = 1; var b = a;//复制 console.log(b)//1 a = 2;//改变a的值 console.log(b)//1
可以看到,我们复制完b以后,即使改变a的值,b也不会改变,因为a和b是相互独立的,按照上面的图,也就是在栈内存中创建了一个变量b 保存的值也是2;
//例子2 var color1 = ["red","green"]; var color2 = color1;//复制 console.log(color2)//["red","green"]; color1.push("black") ;//改变color1的值 console.log(color2)//["red","green","black"]
在例子2中,我们按照完全相同的步骤,操作了一个数组,但是返回的结果却完全不一样,因为此时的复制,实际上是这样:
我们只是复制了一次引用类型的地址而已,所以,不管接下来我们是操作color1还是color2,本质上都是操作同一个数组对象。
刚刚说到,简单的赋值没有办法复制引用类型,那如果我们就是想复制上面的color1数组怎么办呢?可以这样:
var color1 = ["red","green"]; var color2 = []; //复制 for(var i = 0;i < color1.length;i++){ color2[i] = color1[i]; } console.log(color2)//["red","green"]; color1.push("black") ;//改变color1的值 console.log(color2)//["red","green"]
这一次我们先创建了一个空数组color2,然后让color2的每个值都和color1对应相等,最后的color1和color2是相互独立的了,满足了我们的需要。当然对于对象类型也是一样的,使用for-in遍历取代这里的for循环即可。
问题真的就这样解决了吗?当然没有,不过以上这种只复制了第一层属性的方式就叫做浅复制,浅复制有什么缺陷呢?我们可以先思考一下,从直接使用=符号赋值进行复制到浅复制,能够复制成功(成功是指复制的结果与复制源完全独立),是因为我们复制的对象都是基本类型,怎么解释呢?
在复制基本数据类型时,我们直接使用=完成复制
在引用类型的时候,我们循环遍历对象,对每个属性或值使用=完成复制
有没有注意到上文的color1例子使用浅复制之所以能够复制成功,是因为数组中的每一项都是基本数据类型(string),所以猜出了浅复制的局限了吗?假如数组中某一项保存的是一个对象,或者是一个数组,又或者对象的某个属性还是一个对象呢?(换句话说就是引用类型的某个属性还是引用类型),如:
var person = { name:"lin", score:{ physics:85, math:99 } }
这个对象的分数score属性就还是一个对象,那我们使用前面提到for-in遍历复制的时候,对score的复制,不就又变成了我们前面提到的只复制了地址的情况吗?再想想浅复制实现的原理,相信大家猜到了深复制实现的方式:对属性中所有引用类型的值,遍历到是基本类型的值为止,从这种方式上,我们很容易就可以想到利用递归来实现深复制。
function deepCopy (obj) { var result; //引用类型分数组和对象分别递归 if (Object.prototype.toString.call(obj) == "[object Array]") { result = [] for (i = 0; i < obj.length; i++) { result[i] = deepCopy(obj[i]) } } else if (Object.prototype.toString.call(obj) == "[object Object]") { result = {} for (var attr in obj) { result[attr] = deepCopy(obj[attr]) } } //值类型直接返回 else { return obj } return result }
上面的函数很简单:对于传入的参数,首先判断是否为引用类型,如果不是,直接返回即可;如果是,循环遍历该对象的属性,如果某个属性还是引用类型,则针对该属性再次调用deepCopy函数,从而完成深复制。
附注对于浅复制,其实还有其他的实现方式,比如数组中concat和slice方法,对于这些还是希望大家自己了解,本本主要针对深浅复制的实现原理进行解析。
小结对于深浅复制的区别,其实核心的关键点就是是只复制了第一属性还是完全复制了所有的属性,可能有些地方写的稍显啰嗦或者描述不当,欢迎提出意见和建议(然而我并不一定会听,哈哈)。如果对读者有帮助,还是希望能点个推荐。以上内容属于个人见解,如果有不同意见,欢迎指出和探讨。请尊重作者的版权,转载请注明出处,如作商用,请与作者联系,感谢!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/82197.html
摘要:二这么分的好处就是在于节省内存资源,便于合理回收内存详解中的深浅复制有了上面的铺垫,那么我们理解起深浅复制就变得容易的许多。 前言 对于前端开发来说,我们经常能够遇到的问题就是js的深浅复制问题,通常情况下我们解决这个问题的方法就是用JSON.parse(JSON.Stringify(xx))转换或者用类似于Inmmutable这种第三方库来进行深复制,但是我们还是要弄懂其中原理,这样...
摘要:中有三种数据结构栈堆队列。前端进击的巨人一执行上下文与执行栈,变量对象中解释执行栈时,举了一个乒乓球盒子的例子,来演示栈的存取方式,这里再举个栗子搭积木。对于基本类型,栈中存储的就是它自身的值,所以新内存空间存储的也是一个值。 面试经常遇到的深浅拷贝,事件轮询,函数调用栈,闭包等容易出错的题目,究其原因,都是跟JavaScript基础知识不牢固有关,下层地基没打好,上层就是豆腐渣工程,...
摘要:深拷贝相比于浅拷贝速度较慢并且花销较大。所以在赋值完成后,在栈内存就有两个指针指向堆内存同一个数据。结果如下扩展运算符只能对一层进行深拷贝如果拷贝的层数超过了一层的话,那么就会进行浅拷贝那么我们可以看到和展开原算符对于深浅拷贝的结果是一样。 JS中数据类型 基本数据类型: undefined、null、Boolean、Number、String和Symbol(ES6) 引用数据类型:...
阅读 1345·2023-04-25 16:45
阅读 1885·2021-11-17 09:33
阅读 2287·2021-09-27 14:04
阅读 900·2019-08-30 15:44
阅读 2616·2019-08-30 14:24
阅读 3393·2019-08-30 13:59
阅读 1678·2019-08-29 17:00
阅读 865·2019-08-29 15:33