深拷贝与浅拷贝的区别,实现深拷贝的几种方法

    xiaoxiao2024-12-29  63

    如何区分深拷贝与浅拷贝,简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短,如果B没变,那就是深拷贝,自食其力。

     

    此篇文章中也会简单阐述到栈堆,基本数据类型与引用数据类型,因为这些概念能更好的让你理解深拷贝与浅拷贝。

    我们来举个浅拷贝例子:

    let a=[0,1,2,3,4], b=a; console.log(a===b); a[0]=1; console.log(a,b);

     

    嗯?明明b复制了a,为啥修改数组a,数组b也跟着变了,这里我不禁陷入了沉思


    那么这里,就得引入基本数据类型与引用数据类型的概念了。

    面试常问,基本数据类型有哪些,number,string,boolean,null,undefined,symbol以及未来ES10新增的BigInt(任意精度整数)七类。

    引用数据类型(Object类)有常规名值对的无序对象{a:1},数组[1,2,3],以及函数等。

    而这两类数据存储分别是这样的:

    a.基本类型--名值存储在栈内存中,例如let a=1;

    当你b=a复制时,栈内存会新开辟一个内存,例如这样:

     

    所以当你此时修改a=2,对b并不会造成影响,因为此时的b已自食其力,翅膀硬了,不受a的影响了。当然,let a=1,b=a;虽然b不受a影响,但这也算不上深拷贝,因为深拷贝本身只针对较为复杂的object类型数据。

    b.引用数据类型--名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值,我们以上面浅拷贝的例子画个图:

    当b=a进行拷贝时,其实复制的是a的引用地址,而并非堆里面的值。

    而当我们a[0]=1时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。

     

    那,要是在堆内存中也开辟一个新的内存专门为b存放值,就像基本类型那样,岂不就达到深拷贝的效果了


    我们怎么去实现深拷贝呢?

     

    一、Js自带的深拷贝方法

    1、这里先看个数组的例子。

    let a=[1,2,3,4], b=a.slice(); a[0]=2; console.log(a,b);

    那是不是说slice方法也是深拷贝了,毕竟b也没受a的影响。注意:深拷贝是会拷贝所有层级的属性,还是这个例子,我们把a改改

    let a=[0,1,[2,3],4], b=a.slice(); a[0]=1; a[2][0]=1; console.log(a,b);

    拷贝的不彻底啊,b对象的一级属性确实不受影响了,但是二级属性还是没能拷贝成功,仍然脱离不了a的控制,说明slice根本不是真正的深拷贝。

    这里引用知乎问答里面的一张图

     

    第一层的属性确实深拷贝,拥有了独立的内存,但更深的属性却仍然公用了地址,所以才会造成上面的问题。

    数组拷贝的几种方法:

    for循环slice concat扩展运算符 let arr1 = [1,2,3]; let [...arr2] = arr1; let arr3 = [...arr1]; console.log(arr2) // [1, 2, 3] console.log(arr3) // [1, 2, 3]

     

    Array.from

     

    2、JSON对象的parse和stringify

    function deepClone(obj){ let _obj = JSON.stringify(obj), objClone = JSON.parse(_obj); return objClone } let a=[0,1,[2,3],4], b=deepClone(a); a[0]=1; a[2][0]=1; console.log(a,b);

    可以看到,这下b是完全不受a的影响了。但是这种方法也有它的缺陷。

     

    总结一下:

    slice()、concat、Array.from()、... 操作符:只能实现一维数组的深拷贝Object.assign():只能实现一维对象的深拷贝JSON.parse(JSON.stringify(obj)):可实现多维对象的深拷贝,但会忽略undefined、任意的函数、symbol 值

     

    3.除了上面的方法之外,我们还可以借用JQ的extend方法。

    $.extend( [deep ], target, object1 [, objectN ] )

    deep表示是否深拷贝,为true为深拷贝,为false,则为浅拷贝

    target Object类型 目标对象,其他对象的成员属性将被附加到该对象上。

    object1  objectN可选。 Object类型 第一个以及第N个被合并的对象。 

    let a=[0,1,[2,3],4], b=$.extend(true,[],a); a[0]=1; a[2][0]=1; console.log(a,b);

    可以看到,效果与上面方法一样,只是需要依赖JQ库。

    可见,JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题,因此我们需要自己实现。

     

    二、深拷贝函数简单写法(递归实现)

    这么我们封装一个深拷贝的函数(PS:只是一个基本实现的展示,并非最佳实践)

    function deepClone(obj){ let objClone = Array.isArray(obj)?[]:{}; if(obj && typeof obj==="object"){ for(key in obj){ if(obj.hasOwnProperty(key)){ //判断ojb子元素是否为对象,如果是,递归复制 if(obj[key]&&typeof obj[key] ==="object"){ objClone[key] = deepClone(obj[key]); }else{ //如果不是,简单复制 objClone[key] = obj[key]; } } } } return objClone; } // 测试用 var obj1 = { x: { m: 1 }, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo"), b: [1,2,3,4,5], c: null }; var obj2 = deepClone(obj1); obj2.x.m = 2; obj2.b[0] = 2; console.log(obj1); console.log(obj2);

    可以看到

    上面的实现用hasOwnProperty() 过滤了原型链上的属性,看下面这个没过滤的:

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

    Reflect 法 

    function deepClone(obj) { if (!isObject(obj)) { throw new Error('obj 不是一个对象!') } let cloneObj = Array.isArray(obj) ? [...obj] : { ...obj } // Reflect.ownKeys()获取所有自有属性key,不管是否可枚举,但不包括继承自原型的属性 Reflect.ownKeys(cloneObj).forEach(key => { cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] }) return cloneObj }

    但上面的深拷贝方法遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈。

    因此需要改进。

     

    三、深拷贝函数改进(防止循环递归)

    解决因循环递归而暴栈的问题,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可。

    function deepClone(obj, parent = null){ // 改进(1) let result = Array.isArray(obj)?[]:{}; let _parent = parent; // 改进(2) while(_parent){ // 改进(3) if(_parent.originalParent === obj){ return _parent.currentParent; } _parent = _parent.parent; } if(obj && typeof obj === "object"){ for(let key in obj){ if(obj.hasOwnProperty(key)){ if(obj[key] && typeof obj[key] === "object"){ result[key] = deepClone(obj[key],{ // 改进(4) originalParent: obj, currentParent: result, parent: parent }); }else{ result[key] = obj[key]; } } } } return result; }

     设置一个哈希表存储已拷贝过的对象同样可以达到同样的目的:

    function deepClone(obj, hash = new WeakMap()) { if (!isObject(obj)) { return obj } // 查表 if (hash.has(obj)) return hash.get(obj) let cloneObj = Array.isArray(obj) ? [] : {} // 哈希表设值 hash.set(obj, cloneObj) Reflect.ownKeys(obj).map(key => { cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key] }) return cloneObj }

    这里我们使用 WeakMap 作为哈希表,因为它的键是弱引用的,而我们这个场景里键恰好是对象,需要弱引用。

     

    四、深拷贝函数最终版(支持基本数据类型、原型链、RegExp、Date类型) 

    function deepClone(obj, parent = null){ let result; // 最后的返回结果 let _parent = parent; // 防止循环引用 while(_parent){ if(_parent.originalParent === obj){ return _parent.currentParent; } _parent = _parent.parent; } if(obj && typeof obj === "object"){ // 返回引用数据类型(null已被判断条件排除)) if(obj instanceof RegExp){ // RegExp类型 result = new RegExp(obj.source, obj.flags) }else if(obj instanceof Date){ // Date类型 result = new Date(obj.getTime()); }else{ if(obj instanceof Array){ // Array类型 result = [] }else{ // Object类型,继承原型链 let proto = Object.getPrototypeOf(obj); result = Object.create(proto); } for(let key in obj){ // Array类型 与 Object类型 的深拷贝 if(obj.hasOwnProperty(key)){ if(obj[key] && typeof obj[key] === "object"){ result[key] = deepClone(obj[key],{ originalParent: obj, currentParent: result, parent: parent }); }else{ result[key] = obj[key]; } } } } }else{ // 返回基本数据类型与Function类型,因为Function不需要深拷贝 return obj } return result; } // 调试用 function construct(){ this.a = 1, this.b = { x:2, y:3, z:[4,5,[6]] }, this.c = [7,8,[9,10]], this.d = new Date(), this.e = /abc/ig, this.f = function(a,b){ return a+b }, this.g = null, this.h = undefined, this.i = "hello", this.j = Symbol("foo") } construct.prototype.str = "I'm prototype" var obj1 = new construct() obj1.k = obj1 obj2 = deepClone(obj1) obj2.b.x = 999 obj2.c[0] = 666 console.log(obj1) console.log(obj2) console.log(obj1.str) console.log(obj2.str)

     

     

    五、深拷贝还可以使用 lodash 的 copyDeep 或 copyDeepWith 方法

     

     

    说了这么多,了解深拷贝也不仅仅是为了应付面试题,在实际开发中也是非常有用的。例如后台返回了一堆数据,你需要对这堆数据做操作,但多人开发情况下,你是没办法明确这堆数据是否有其它功能也需要使用,直接修改可能会造成隐性问题,深拷贝能帮你更安全安心的去操作数据,根据实际情况来使用深拷贝,大概就是这个意思。

    最新回复(0)