详解js深浅复制

前言

在以前写继承的过程谈到了深浅复制的问题,由于有读者反映到须要解析,趁今天周末写一篇解析,今天的主体相对以前来讲理解难度低一些,篇幅可能也比较短,诸君按需阅读便可。数组

从两种数据类型提及

在js中,变量的类型能够大体分红两种:基本数据类型和引用数据类型,其中基本数据类型指的是简单的数据段,包括:函数

  • Undefined
  • Null
  • Boolean
  • Number
  • String(字符串在一些其余语言中是被当作对象使用的,属于引用类型,但在js里是基本类型)

而引用类型的值指的是可能包含多个值的对象。可能上面这种描述你们都看过很多,可是有没有思考过为何要把数据类型这样分呢?本质上,是由于基本数据类型保存在栈内存,而引用类型保存在堆内存中。那再进一步问:为何要分两种保存方式呢? 根本缘由在于保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,可是咱们能够把它的地址写在占内存中以供咱们访问。举个例子:spa

var a = 1;//定义了一个number类型
var obj1 = {//定义了一个objr类型
    name:'obj'
};

在执行这段代码后,内存空间里是这样的:
栈内存和堆内存
由于这种保存方式的存在,因此咱们在操做变量的时候,若是是基本数据类型,则按值访问,操做的就是变量保存的值;若是是引用类型的值,咱们只是经过保存在变量中的引用类型的地址类操做实际对象。从而也引出了所谓的深浅复制问题。prototype

深复制和浅复制

不一样的复制方式

紧接着上文的内容,假设有如下代码:code

//例子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,本质上都是操做同一个数组对象blog

深复制和浅复制

刚刚说到,简单的赋值没有办法复制引用类型,那若是咱们就是想复制上面的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对应相等,最后的color1color2是相互独立的了,知足了咱们的须要。固然对于对象类型也是同样的,使用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函数,从而完成深复制。

附注

对于浅复制,其实还有其余的实现方式,好比数组中concatslice方法,对于这些仍是但愿你们本身了解,本本主要针对深浅复制的实现原理进行解析。

小结

对于深浅复制的区别,其实核心的关键点就是是只复制了第一属性仍是彻底复制了全部的属性,可能有些地方写的稍显啰嗦或者描述不当,欢迎提出意见和建议(然而我并不必定会听,哈哈)。若是对读者有帮助,仍是但愿能点个推荐。以上内容属于我的看法,若是有不一样意见,欢迎指出和探讨。请尊重做者的版权,转载请注明出处,如做商用,请与做者联系,感谢!

相关文章
相关标签/搜索