拷贝:给定一个对象 ,复制一份一模一样独立的对象来。它又分成浅拷贝和深拷贝。
简介
本文介绍了js中深拷贝的实现方法,主要用递归加weak
主要内容:
- 浅拷贝与深拷贝的区别
- 递归实现深拷贝
- 处理循环引用
- 处理特殊对象
- jquery和lodash中的深拷贝
浅拷贝(shallow clone)
1 var obj = {
2 name: '小王',
3 hobby:['reading','running']
4 }
目标:把obj这个对象复制一份,得到obj1。保证obj与obj1是一样的,且相互独立的两个对象
思路:把obj中的属性全取来,做循环,把属性值赋给obj1
示例代码
1 // 思路:
2 // 把obj中的属性全取来,做循环,把属性值赋给obj1
3 function clone(obj)
4 var obj1 = {};
5 // 把obj中的属性全取出来 for in
6 for(var key in obj){
7 // key是obj中的属性名,
8 // 让obj1中也要有这个属性,并且值与obj是相同的
9 // console.log(key);
10 obj1[key] = obj[key]
11\
12 }
13 return obj1
14}
这里是浅拷贝。
这里是浅拷贝, 浅拷贝只能拷贝一层。如果对象中只有一层( 对象的属性值不熟对象与数组 )这是可以正常工作的。如果对象有多层( 理解为多维数组) 它就不能实现真正的"拷贝"功能了。
常见的浅拷贝
有如下三种:
- Object.assign()
- 扩展运算符。
var cloneObj = { ...obj }; - Array.prototype.slice()。slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
深拷贝 (deep clone)
在浅拷贝的基础上,对当前要复制的内容进行判断:如果仍是一个对象,则再次调用拷贝函数。
1 var obj = {
2 name: '小王',
3 hobby:['reading','running']
4 }
5
6 // 对于当前要拷贝的内容,如果还是一个对象,则要递归调用,而不是直接赋值
7 function deepCopy(obj){
8 // 定义一个空的对象或者是数组
9 // 如果当前要复制的是数组就用一个空数组,或者用一个空对象。
10 // var tempObj = Array.isArray(obj) ? [] : {}
11 var tempObj = {}
12 if( Array.isArray(obj) ) {
13 tempObj = []
14 }
15
16 // var tempObj = {}
17 // .... 操作
18 for(var key in obj) {
19 var t = obj[key]
20 // 当要拷贝的内容还是一个对象或者是数组
21 if(Array.isArray(t) || typeof t === "object") {
22 tempObj[key] = deepCopy(obj[key])
23 }
24 else // 当要拷贝的内容只是一个基本类型的数据
25 {
26 tempObj[key] = t
27 }
28 }
29 return tempObj
30}
31
32 var obj1 = deepCopy(obj);
33 // 验证:
34 // 给 obj1.hobby.添一个信息,不会影响obj.hobby.
35 obj1.hobby === obj.hobby; // false
上面的代码可以简化如下:
1 function isObj(obj) {
2 return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
3 }
4 function deepCopy(obj) {
5 var tempObj = Array.isArray(obj) ? [] : {}
6 for(var key in obj) {
7 tempObj[key] = isObj(obj[key]) ? deepCopy(obj[key]) : obj[key]
8 }
9 return tempObj
10 }
上面的代码在没有遇到循环引用之前都是正常工作的。下面将来介绍循环引用 。
循环引用
下面来看一个循环引用的例子:
注意下面第5行代码,obj.a还是指向了obj,这里就构成了一个环。
1 var obj = {
2 name: '小王',
3 hobby: ['reading', 'running'],
4 }
5 obj.a = obj; // 手动加上循环引用
6 function isObj(obj) {
7 return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
8 }
9 function deepCopy(obj) {
10 var tempObj = Array.isArray(obj) ? [] : {}
11 for (var key in obj) {
12 tempObj[key] = isObj(obj[key]) ? deepCopy(obj[key]) : obj[key]
13 }
14 return tempObj
15 }
16
17 var obj1 = deepCopy(obj)
18 console.log(obj);
19 console.log(obj1);
20
21 console.log(obj1.hobby === obj.hobby);
导致的后果就是:
解决思路:如果当前要处理的是一个对象,就先去检查这个对象是否已经处理过了。
这里可以借用ES6中新增加的数据结构:WeakMap来记录当前的对象是否已经处理过了。WeakMap是一个集合,其中每一个元素都是键值对结构,这一点很像对象,但它的特殊之处在于它的键是对象。
基本的用法如下:
1 var wm1 = new WeakMap();
2 var key = {foo: 1};
3 wm1.set(key, 2);
4 wm1.get(key); // 2
5 wm1.set([1,2],0)
6 wm1.get([1,2])// undefined
引一个全局变量hash来保存已经处理过的对象。
1 var hash = new WeakMap()
2 function deepCopy(obj) {
3 if(hash.has(obj)){
4 return hash.get(obj)
5 }
6 var tempObj = Array.isArray(obj) ? [] : {}
7 hash.set(obj,tempObj)
8 for (var key in obj) {
9 tempObj[key] = isObj(obj[key]) ? deepCopy(obj[key],hash) : obj[key]
10 }
11 return tempObj
12 }
或者是如下优化后的代码
1 function deepCopy(obj, hash = new WeakMap()) {
2 if(hash.has(obj)) return hash.get(obj)
3 var cloneObj = Array.isArray(obj) ? [] : {}
4 hash.set(obj, cloneObj)
5 for (var key in obj) {
6 cloneObj[key] = isObj(obj[key]) ? deepCopy(obj[key], hash) : obj[key];
7 }
8 return cloneObj
9 }
特殊对象的处理
1 var obj = {
2 reg: /abc/ig,
3 date: new Date()
4 }
如上对象中,有两个属性,每个属性值都是一个对象。
如果使用上述的代码来实现
1 function deepCopy(obj, hash = new WeakMap()) {
2 if(hash.has(obj)) return hash.get(obj)
3 var cloneObj = Array.isArray(obj) ? [] : {}
4 hash.set(obj, cloneObj)
5 for (var key in obj) {
6 cloneObj[key] = isObj(obj[key]) ? deepCopy(obj[key], hash) : obj[key];
7 }
8 return cloneObj
9 }
将会出现致命的错误:
原因是:对于
reg: /abc/ig这个键值对,它的值确实是一个对象,即使用isObj方法去检查它,它是满足条件的,毕竟正则对象也是对象嘛!可是,对于/abc/ig 这个对象,for in循环却无能为力,无法正确地得到拷贝之后的结果,而只能是得到一个空对象。
同理分析日期对象。
要解决这个问题,思路也很简单:对于日期和正则单独处理!
可以使用如下代码来解决这个问题:
1 function deepClone(obj, hash = new WeakMap()) {
2 if (null == obj || "object" != typeof obj) return obj;
3 var cloneObj
4 var Constructor = obj.constructor
5
6 switch (Constructor) {
7 case RegExp:
8 cloneObj = new Constructor(obj)
9 break
10 case Date:
11 cloneObj = new Constructor(obj.getTime())
12 break
13 default:
14 if (hash.has(obj)) return hash.get(obj)
15 cloneObj = new Constructor()
16 hash.set(obj, cloneObj)
17 console.log(2, hash.get(obj))
18 }
19 for (var key in obj) {
20
21 cloneObj[key] = isObj(obj[key]) ? deepClone(obj[key], hash) : obj[key];
22
23 }
24 return cloneObj
25 }
jquery的深拷贝和lodash的深拷贝
jquery.js和lodash.js中都提供了深拷贝的方法。
分别是:$.extend(), _.cloneDeep()。下面测试了它们对深拷贝的支持。
结论是:
- loadash和jquery的拷贝都解决日期对象和正则对象的问题;
- lodash的深拷贝能解决循环引用的问题,而jquery的深拷贝不能解决循环引用问题。
测试代码如下:
1 var obj = {
2 name: '小王',
3 hobby: ['reading', 'running'],
4 }
5 obj.a = obj; // 手动加上循环引用
lodash的测试代码:
1 var obj2 = _.cloneDeep(obj);
jquery的测试代码如下:
1 var obj2 = {};
2 $.extend(true,obj2,obj)
其中采用jquery及lodash的版本如下:4.17.15/lodash.js,3.4.1/jquery.js
小结
本文讨论了js中浅拷贝与深拷贝的区别,并实现了一下深拷贝的代码,最后比较了jquery和lodash中的深拷贝。