聊一聊JS深复制和浅复制(一)

大家可以查到很多的关于深浅复制的文章,有很多都写得很好,我这里就对自己学习到的做一做总结,争取以实例来详细说明JS复制由浅入深复制的整个过程。

查阅了很多的资料,也看到了很多的复制方法,能够完成复制的深度大概可以由下面的这幅图来直观的看出。

方法 基本类型 Function Array Object Date Error RegExp Symbol 键 循环引用 原型链上的属性
.... ? ? ? ? ? ? ? ? ? ?
.... ? ? ? ? ? ? ? ? ? ?
..... ? ? ? ? ? ? ? ? ? ?

下面的对象obj为复制目标,isObject是工具方法:

function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
}
var h = Symbol('我是一个Symbol');
var obj = {
    a: "hello",
    a1: null,
    a2: undefined,
    b:{
        a: "world",
        b: 21
    },
    c:["Bob", "Tom", "Jenny"],
    d:function() {
        alert("hello world");
    },
    e: new Date('2012-12-12'),
    f: new RegExp('\.'),
    g: new Error('error')
};
obj[h] = 'symbol';
/*
    这里把复制的方法加进来
*/
var cloneObj = deepClone(obj);
obj.a='helloForin';
obj.c[0]='chaichai';
obj.b.a='worldForin';
console.log(obj);
console.log(cloneObj);

上面是测试demo的模板,每次只需把深浅复制的方法定义在注释的地方即可运行查看结果。

1.浅复制

浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。

1.1 Array的slice和concat方法

Array的slice和concat方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用了,因此这两个方法被我归为浅复制

var array = [1, [1,2,3], {name:"array"}];
var array_concat = array.concat();
var array_slice = array.slice(0);
//改变array_concat中数组元素的值
array_concat[1][0] = 5;
console.log(array[1]);    //[5,2,3]
console.log(array_slice[1]);  //[5,2,3]
//改变array_slice中对象元素的值
array_slice[2].name = "array_slice";
console.log(array[2].name);   //array_slice
console.log(array_concat[2].name); //array_slice

1.2 Object.assign()

var obj = { a: {a: "hello", b: 21} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "changed";
console.log(obj.a.a); // "changed"

2.深复制

2.1深复制基本功

2.1.1迭代递归法
2.1.1.1 for...in基本功
// 迭代递归法:深拷贝对象与数组
function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }
    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [] : {}
    for (let key in obj) {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
    }
    return cloneObj
}

运行结果:

forin基本功.png

对比复制前后的对象,可以发现该方法:

方法 基本类型(除null、undefined) null undefined Function Array Object Date Error RegExp Symbol 键 循环引用 原型链上的属性
for...in基本功 yes yes yes no yes yes no no no no ?
2.1.1.2 Reflect基本功
function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }
    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [...obj] : { ...obj }
    Reflect.ownKeys(cloneObj).forEach(key => {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
    })
    return cloneObj
}

运行结果:

Reflect基本功.png

对比复制前后的对象,可以发现该方法:

方法 基本类型(除null、undefined) null undefined Function Array Object Date Error RegExp Symbol 键 循环引用 原型链上的属性
Reflect基本功 yes yes yes no yes yes no no no yes ?
2.1.1.3 序列化反序列化基本功
function deepClone(obj){
    let _obj = JSON.stringify(obj),
        objClone = JSON.parse(_obj);
    return objClone
}

运行结果:

序列化反序列化基本功.png

对比复制前后的对象,可以发现该方法:

方法 基本类型(除null、undefined) null undefined Function Array Object Date Error RegExp Symbol 键 循环引用 原型链上的属性
序列化反序列化基本功 yes no no no yes yes no no no no ?

2.2深复制内功

2.2.1 lodash中深拷贝内功

调用lodash中的cloneDeep方法:let result = _.cloneDeep(obj),

var cloneObj = _.cloneDeep(obj);
obj.a='helloForin';
obj.c[0]='chaichai';
obj.b.a='worldForin';
obj.d='fff'
console.log('func相同么? ' + obj.d == cloneObj.d);
console.log('Error相同么? ' + obj.g == cloneObj.g);
console.log(obj);
console.log(cloneObj);

运行结果

lodash内功.png

可以发现:

方法 基本类型(除null、undefined) null undefined Function Array Object Date Error RegExp Symbol 键 循环引用 原型链上的属性
lodash中深拷贝内功 yes yes yes yes yes yes yes yes yes yes ?
2.2.2 jQuery.extend()内功

调用lodash中的cloneDeep方法:var cloneObj = $.extend(true,cloneObj, obj);

var cloneObj = $.extend(true,cloneObj, obj);
obj.a='helloForin';
obj.c[0]='chaichai';
obj.b.a='worldForin';
console.log('func相同么? ' + obj.d == cloneObj.d);
console.log('Error相同么? ' + obj.g == cloneObj.g);
obj.d='fff'
console.log(obj);
console.log(cloneObj);

运行结果:

![lodash_loopobj_success.png](https://upload-images.jianshu.io/upload_images/14720179-e183a655de9b40d5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

对比结果,可以得到:

方法 基本类型(除null、undefined) null undefined Function Array Object Date Error RegExp Symbol 键 循环引用 原型链上的属性
jQuery.extend()内功 yes yes no yes yes yes yes yes yes no ?

2.3 循环引用

若复制的源对象中有一个属性引用该源对象obj.loopObj=obj;,那么经测试可以发现以上五种方法只有lodash中深拷贝内功可以解决。

下面给出jQuery.extend()内功在这种情况下的运行结果:

jquery_loopobj_error.png

lodash中深拷贝内功在这种情况成功复制:

lodash_loopobj_success.png

查看lodash的源码可以发现,这种方法可以实现的原因是:lodash 使用的是栈把对象存储起来了,如果有环对象,就会从栈里检测到,从而直接返回结果,悬崖勒马。这种算法思想来源于 HTML5 规范定义的结构化克隆算法,它同时也解释了为什么 lodash 不对 Error 和 Function 类型进行拷贝。

2.3.1 进阶

for...in为例,查看lodash的源码之后,知道原理就可以对for...in基本功进行改造,利用哈希表存储已拷贝过的对象用来判断:

function deepClone(obj, hash = new WeakMap()) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }
    if (hash.has(obj)) return hash.get(obj);
    
    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [] : {}

    hash.set(obj, cloneObj)

    for (let key in obj) {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key]
    }
    return cloneObj
}

运行结果:

forin_loop_success.png

2.4 原型属性的复制

这部分待研究,可以知道的是for...in是可以追踪到原型上面的属性的。

2.5 总结

方法 基本类型(除null、undefined) null undefined Function Array Object Date Error RegExp Symbol 键 循环引用 原型链上的属性
lodash中深拷贝内功 yes yes yes yes yes yes yes yes yes yes yes
序列化反序列化基本功 yes no no no yes yes no no no no no
Reflect基本功 yes yes yes no yes yes no no no yes no
for...in基本功 yes yes yes no yes yes no no no no no
jQuery.extend()内功 yes yes no yes yes yes yes yes yes no ?

你可能感兴趣的:(聊一聊JS深复制和浅复制(一))