JavaScript深拷贝初探

 

  今天和大家一起分享在JavaScript中如何实现深拷贝。

 

  0. 为什么要实现深拷贝

  在之前的一篇文章中  JavaScript变量存储浅析(二) 我们已经知道,在JS中,如果只是将一个对象简单的赋值给另外一个对象,那么拷贝的实际上只是对象在堆内存中的地址而已,也就是说,拷贝后的对象仍然和源对象指向同一个内存中的对象,只是修改其中一个对象,那么另外一个对象也会随之被修改。

  我们通过一个简单的例子来解释这个问题:

1 var obj1={
2         attr:100
3     };
4     var obj2=obj1;    //简单复制obj1对象
5     obj2.attr=200;    //修改obj2属性
6     console.log(obj1.attr);    //输出200,说明obj1也被修改

  OK,这就是深拷贝的意义所在。完整的拷贝一个对象的所有属性,而不是引用地址。

  数组是这样的情况吗?大家可以自行验证一下!

  

  1. 数据类型判断

  实现深拷贝的第一步就是判断数据类型。

  我们都知道,JavaScript的数据类型分为两大类:

  •   基本类型:String,Number,Boolean,undefined,Null
  •   引用类型:Object,Array,Date,Reg,Function等

  对于基本类型的判断,我们使用typeof就可以,对于实例类型,也可以通过instanceof来判断。

  除了这两个方法以外,我们还有一些别的方式来判断,就是Object下的toString方法.

  偷个懒,从MDN上查询下该方法的调用:

  

    也就是说,我们只需要截取返回值的type值就可以了。下面是一个参考方法:

1 var util={
2         getType:function(o){    //判断对象类型
3             var _t;
4             return ((_t = typeof(o)) == "object" ? o==null && "null" || Object.prototype.toString.call(o).slice(8,-1):_t).toLowerCase();
5         }
6     };

  我们先定义了一个util对象用于存放本节需要使用的相关方法,getType方法用于检测对象类型。

  实现的原理上面也提及了,如果是基本类型的话,就直接返回typeof值。如果是对象类型,我们还需要进行细分,从第8个字符开始截取到倒数第二个字符作为返回值。

  这个方法大家可以实际操作和验证一下。

 

  2. 深拷贝

  一般情况下,我们主要解决以下引用类型的深度拷贝:

  • 对象:遍历对象的所有属性,将其值拷贝到目标元素的对应属性上。
  • 数组:遍历数组的所有元素,将其值分别拷贝到目标数组的对象index下。
  • 函数:一般来说不作特殊处理,如果需要的话可以先将function通过tostring方法转换为字符串,然后再调用eval还原函数。

  本文的重点在于对象与数组的深度拷贝上。

  我们在util对象上添加了deepClone方法用于实现深拷贝,需要传入源对象作为参数。

  

 1 var util={
 2         getType:function(o){    //判断对象类型
 3             var _t;
 4             return ((_t = typeof(o)) == "object" ? o==null && "null" || Object.prototype.toString.call(o).slice(8,-1):_t).toLowerCase();
 5         },
 6         deepClone:function(source){    //深拷贝
 7             var self=this;    //保存当前对象引用
 8             var destination=self.getType(source);
 9             destination=destination==='array'?[]:(destination==='object'?{}:source);
10             for (var p in source) {
11                 if (self.getType(source[p]) === "array" || self.getType(source[p]) === "object") {
12                     destination[p] = self.getType(source[p]) === "array" ? [] : {};
13                     destination[p]=arguments.callee.call(self, source[p]);    //使用call修改函数的作用域
14                 } else {
15                     destination[p] = source[p];
16                 }
17             }
18             return destination;
19         }
20     };
  • 第7行:保存当前对象的引用,便于后面做递归调用的时候修改作用域。  
  • 第8行:拿到source源对象的类型。
  • 第9行:如果类型为数组的话,我们就创建一个空数组;如果是对象的话,创建一个空对象;然后将创建的空对象或空数组赋值到destination这个局部变量上。如果不是这两种类型的话,那就不属于深拷贝的范围,我们直接将源对象的值赋值回去。
  • 第10-11行:使用for..in对源对象进行循环,遍历其所有元素或属性。
  • 第12行:同样的,根据每个属性值的类型,在destination创建一个对应的空对象或空数组。
  • 第13行:使用callee进行函数的递归调用,再次计算每个属性或元素的值。

  2015-12-24 更新:这里增加了call函数来递归调用当前函数的引用,并且修改了其作用域。

  特别鸣谢:感谢 rookieCat 指出了使用callee 时的作用域问题。

  • 第18行:返回局部变量destination。

  整个的实现核心就在于我们要清楚需要处理哪些类型的数据,以及使用callee进行递归调用

  好的,下面我们来使用深拷贝方法,看能否达到想要的效果:

1 var obj1={
2         attr:100
3     };
4 
5     
6     var obj2=util.deepClone(obj1);    //将obj1深拷贝到obj2
7     obj2.attr=200;    //修改obj2的属性值
8     console.log(obj1.attr);    //obj1属性值未发生变化

  最终的结果是:通过深拷贝得到的新对象在内存中有独立的存储位置,因此修改新对象不会对源对象造成任何影响。

  

  

  

  

posted @ 2015-12-23 07:33  大魔王萨格拉斯  阅读(1360)  评论(7编辑  收藏  举报