js进阶-浅拷贝与深拷贝

759 阅读5分钟
拷贝:给定一个对象 ,复制一份一模一样独立的对象来。它又分成浅拷贝和深拷贝。

简介

本文介绍了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}

这里是浅拷贝。

微信截图_20220611193609.png 这里是浅拷贝, 浅拷贝只能拷贝一层。如果对象中只有一层( 对象的属性值不熟对象与数组 )这是可以正常工作的。如果对象有多层( 理解为多维数组) 它就不能实现真正的"拷贝"功能了。

常见的浅拷贝

有如下三种:

  • 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);

1.bmp 导致的后果就是:

微信图片_20220611194540.png 解决思路:如果当前要处理的是一个对象,就先去检查这个对象是否已经处理过了。

这里可以借用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    datenew 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 }

将会出现致命的错误:

微信截图_20220611194850.png 原因是:对于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)

22.png 其中采用jquery及lodash的版本如下:4.17.15/lodash.js,3.4.1/jquery.js

小结

本文讨论了js中浅拷贝与深拷贝的区别,并实现了一下深拷贝的代码,最后比较了jquery和lodash中的深拷贝。