JavaScript深拷贝全解

什么是深拷贝?

深浅拷贝都是对于复杂数据类型Object来说的,而对象涉及到传值传址的问题
下列浅拷贝

let obj1 = {
  a: 1,
  b: 2,
}

let obj2 = obj1

浅拷贝就是单纯的传值,obj1对象的地址值赋值给obj2
这就会导致obj1,obj2操作的是同一个对象

深拷贝就是在堆内存中重新开辟一块地址空间,实现两者互不影响的拷贝

方法一:JSON序列化与反序列化

var obj = {
  a: 1,
  b: [1,2,3],
  c: {
    d: 4,
    e: 5,
  },
}

var obj2 = JSON.parse(JSON.stringify(obj))

obj2.a = 2;
obj2.c.e = 555;
console.log(obj, obj2)

在这里插入图片描述

缺点

  1. 不支持function

  2. 不支持undefined
    在这里插入图片描述

  3. 不支持引用,环状结构

    var obj = {
      name: 'lc'
    }
    obj.self = obj;
    var obj2 = JSON.parse(JSON.stringify(obj))
    
    console.log(obj, obj2)
    

    在这里插入图片描述

    1. 不支持正则表达式
      在这里插入图片描述

JSON不支持的数据类型都无法拷贝
JSON只支持:string, boolean, array, object, null五种类型

方法二:递归克隆

思路

递归

  1. 看结点的类型(7种)number,string,bool,undefined,null,object,symbol(arry以及function是object的子类型)
  2. 如果是基本类型就直接赋值
  3. 如果是object就分情况讨论

object

  1. 普通 object:for in
    for in 有个问题,他会遍历原型里的键
  2. 数组 Array初始化
  3. 函数,闭包如何拷贝?
  4. 日期Date 如何拷贝?

实现

1 拷贝普通类型

function deepClone(source) {
    return source;
}

2 拷贝对象object以及数组类型

function deepClone(source) {
    if (source instanceof Object) {
        // 判断是对象还是子类型数组,分别处理返回结果的类型
        const dist = source instanceof Array ? new Array() : new Object()
        for (let key in source) {    // for in 会遍历source原型上的属性
            dist[key] = deepClone(source[key]);
        }
        return dist
    }
    return source;
}

3 拷贝函数function

function deepClone(source) {
    if (source instanceof Object) {
        let dist = {};
        if (source instanceof Array) {
            dist = [];
        } else if (source instanceof Function) {
            dist = function () {
                return source.apply(this, arguments);
            }
        }
        for (let key in source) {    // for in 会遍历source原型上的属性
            dist[key] = deepClone(source[key]);
        }
        return dist
    }
    return source;
}

let source = {
    deep: 1,
    common: '普通数据类型',
    arr: [1, 2, 3],
    obj: {
        deep: 2,
        obj: {
            deep: 3,
        }
    },
    func: function (x, y) { return x + y },
}
let source2 = deepClone(source);
// console.log(source)
// console.log(source2)
console.log(source.func(1, 2))
console.log(source2.func(1, 2))

4. 解决闭环问题

闭环问题

以上对象总会有一个结尾,递归会终止
但是如果对象有一个环,就会死循环
比如说window对象其实就是一个环
在这里插入图片描述

let source = {
    circle: {},
}
source.circle.self = source.circle;

let source2 = deepClone(source);

在这里插入图片描述

解决

记录已经遍历过的对象,每次深拷贝之前判断是否走过就行了
如果已经是拷贝过的,注意要返回的是之前已经拷贝过的对象
这样才能实现独立的新环
所以保存原对象时要同时保存拷贝对象

let cache = []; // 保存已经克隆的对象

function findCache(source) {
    for (let i = 0; i < cache.length; i++) {
        if (cache[i][0] === source) {
            // 返回之前的拷贝
            return cache[i][1];
        }
    }
}
function deepClone(source) {
    if (source instanceof Object) {
        let cacheDist = findCache(source);
        // 判断是否拷贝过
        if (cacheDist) {
            return cacheDist;
        }

        let dist = {};  // 默认为对象
        if (source instanceof Array) {
            dist = [];
        } else if (source instanceof Function) {
            dist = function () {
                return source.apply(this, arguments);
            }
        }
        cache.push([source, dist]);

        for (let key in source) {    // for in 会遍历source原型上的属性
            dist[key] = deepClone(source[key]);
        }
        return dist;
    }
    return source;
}

circle = {
    a: 1
}
circle.self = circle;

let circle2 = deepClone(circle);
circle2.a = 1111111;
console.log(circle.self)
console.log(circle2.self)
console.log(circle !== circle2)
console.log(circle.a === circle2.a)
console.log(circle.self !== circle2.self)

5 解决爆栈问题(一般不考虑)

如果一个对象里有很多对象,递归会调用栈,如果栈的长度高于2万,就会出现爆栈问题
通过for循环模拟一个无限级对象

let a = {
    child: null,
}
let b = a;
for (let i = 0; i < 20000; i++) {
    b.child = {
        child: null
    }
    b = b.child;
}
解决方法

将竖向的递归改为横向的队列,依次深拷贝,一般来说不用考虑爆栈

6 拷贝正则表达式和Date()

正则表达式

有两个重要属性
在这里插入图片描述
只需要获取这两个属性重新声明正则就行了

 else if (source instanceof RegExp) {
     dist = new RegExp(source.source, source.flags)
 }
拷贝Date()
else if (source instanceof Date) {
  dist = new Date(source);
}
由此可得

如果对象是一些特殊子对象
只能else if 来判断,并new一个新的这个特殊对象

完整代码

let cache = []; // 保存已经克隆的对象

function findCache(source) {
    for (let i = 0; i < cache.length; i++) {
        if (cache[i][0] === source) {
            // 返回之前的拷贝
            return cache[i][1];
        }
    }
}
function deepClone(source) {
    if (source instanceof Object) {
        let cacheDist = findCache(source);
        // 判断是否拷贝过
        if (cacheDist) {
            return cacheDist;
        }

        let dist = {};  // 默认为对象
        if (source instanceof Array) {
            dist = [];
        } else if (source instanceof Function) {
            dist = function () {
                return source.apply(this, arguments);
            }
        } else if (source instanceof RegExp) {
            dist = new RegExp(source.source, source.flags)
        } else if (source instanceof Date) {
            dist = new Date(source);
        }
        cache.push([source, dist]);

        for (let key in source) {    // for in 会遍历source原型上的属性
            // 如果不是本身的属性就跳过
            if (!source.hasOwnProperty(key)) continue;
            dist[key] = deepClone(source[key]);
        }
        return dist;
    }
    return source;
}


其他问题(面试技巧)

可以留下这些问题给面试官,并且想好解决方案

1 是否需要拷贝原型属性?

  • 不需要,内存太大
  • 所以需要跳过原型属性
  • 解决for in 遍历原型的问题

给源对象一个原型属性

let a = Object.create({ name: 'a' });
a.xxx = { yyy: { zzz: 1 } };
let b = deepClone(a);
console.log(a.__proto__)
console.log(b.__proto__)

深拷贝的遍历中,不是自身属性就跳过

if (!source.hasOwnProperty(key)) continue;

2 解决目前cache的问题

cache在进行一次深拷贝后没有清空,会造成全局变量污染问题
面向对象来解决,封装成一个class

class Cloner {
    constructor() {
        this.cache = []; // 保存已经克隆的对象
    }

    findCache(source) {
        for (let i = 0; i < this.cache.length; i++) {
            if (this.cache[i][0] === source) {
                // 返回之前的拷贝
                return this.cache[i][1];
            }
        }
    }
    deepClone(source) {
        if (source instanceof Object) {
            let cacheDist = this.findCache(source);
            // 判断是否拷贝过
            if (cacheDist) {
                return cacheDist;
            }

            let dist = {};  // 默认为对象
            if (source instanceof Array) {
                dist = [];
            } else if (source instanceof Function) {
                dist = function () {
                    return source.apply(this, arguments);
                }
            } else if (source instanceof RegExp) {
                dist = new RegExp(source.source, source.flags)
            } else if (source instanceof Date) {
                dist = new Date(source);
            }
            this.cache.push([source, dist]);

            for (let key in source) {    // for in 会遍历source原型上的属性
                // 如果不是本身的属性就跳过
                if (!source.hasOwnProperty(key)) continue;
                dist[key] = this.deepClone(source[key]);
            }
            return dist;
        }
        return source;
    }
}


let obj1 = {
    a:1,
    obj: {
        a: 1,
    },
    func: function(){},
}
let dc = new Cloner();
let obj2 = dc.deepClone(obj1);
obj2.a = 111111;
obj2.obj.a = 2222222;
obj2.func = function(){console.log('成功')};
console.log(obj1)
console.log(obj2)
obj2.func();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值