JavaScript实现深拷贝

前言

网上关于深浅拷贝的定义有很多,以下是摘自ChatGPT的回答:

浅拷贝 : 浅拷贝是指创建一个新的对象,新对象的属性值和原始对象的属性值相同,但是对于属性值为对象的情况,新对象仍然引用这些对象的地址。换句话说,浅拷贝只是复制了对象的第一层结构,而没有递归地复制更深层的对象。

举个例子,如果有一个对象 A,其中包含一个属性 B,而 B 又是一个对象。那么进行浅拷贝后的对象如果修改了 B 对象中的属性,原始对象和浅拷贝对象都会受到影响。

深拷贝 : 深拷贝是指创建一个新的对象,新对象的所有属性值和原始对象的属性值完全相同,包括所有层级的嵌套对象。换句话说,深拷贝会递归地复制对象的所有层级,确保新对象和原始对象完全独立,互不影响。

浅拷贝

JavaScript实现浅拷贝的方法有很多,以下列举几个比较常见的:

  • Object.assign()
  • 扩展运算符
  • 数组实例上的若干方法(map, filter, slice, concat)等

深拷贝

JSON.stringify, JSON.parse

最简单的方法是 JSON.parse(JSON.stringify(obj)),它的使用方法如下:

js 复制代码
const obj = {
  a: 1,
  b: 2
}

const cloneObj = JSON.parse(JSON.stringify(obj));
cloneObj.a = 2;
console.log(obj); // { a: 1, b: 2 }

但是,这种方法有缺点:

  • 会忽略undefinedSymbol函数
  • NaNInfinity会被序列化成null
  • 原型链丢失
  • 无法处理循环引用问题

这种方法可以用于日常开发中深拷贝简单对象的场景。接下来,我们试着实现一个深拷贝,并处理各种边界情况

第一层拷贝

先实现第一层拷贝

js 复制代码
const clone = (target) => {
   // 定义一个拷贝对象
   const cloneTarget = {};
   for(const key in target) {
       cloneTarget[key] = target[key];
   }
   return cloneTarget;
}

克隆后,原对象更深层的对象会共享同一地址

js 复制代码
const obj = {
    a: 1, 
    b: {
        c: 2, 
        d: 3
    }, 
    e: 4
};
const newObj = clone(obj);

newObj.b.c = 444;
console.log(obj);   // {a: 1, b: {c: 444, d: 3}, e: 4}
console.log(newObj) // {a: 1, b: {c: 444, d: 3}, e: 4}

使用递归

在上面的基础上加入递归,处理深层对象没有拷贝的问题

js 复制代码
const deepClone = (target) => {
    // null或基本数据类型则直接返回
    if(target === null || typeof target !== "object") {
        return target;
    }
    // 定义一个拷贝对象
    const cloneTarget = {};
    
    for(const key in target) {
        // 递归拷贝
        cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
}

测试:

js 复制代码
const obj = {
    a: 1, 
    b: {
        c: 2, 
        d: 3
    }, 
    e: 4
};
const newObj = clone(obj);

newObj.b.c = 444;
console.log(obj);   // {a: 1, b: {c: 2, d: 3}, e: 4}
console.log(newObj) // {a: 1, b: {c: 444, d: 3}, e: 4}

深层对象也不共享内存地址了

处理其它引用类型

递归可以解决拷贝目标为普通对象的拷贝问题,但如果拷贝目标是Array、Date、RegExp这些非普通对象,就需要进行特殊处理

js 复制代码
const deepClone = (target) => {
    // null或基本数据类型则直接返回
    if(target === null || typeof target !== "object") {
        return target;
    }
    // 若拷贝对象诸如Date, RegExp这些特殊引用类型,则需要创建新的实例返回
    if(target instanceof Date) return new Date(Date);
    if(target instanceof RegExp) return new RegExp(target);
    // 其它引用类型处理...
    
    // 创建拷贝对象
    const cloneTarget = Array.isArray(target) ? [] : {};
    
    for(const key in target) {
        // 递归拷贝
        cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
}

上述代码演示了对Date, Regexp和Arrary三种特殊类型的处理,但Js内置的引用类型比较多,可以参考Loadsh深拷贝函数源码对这些类型的定义:

处理Symbol类型

for...in循环无法遍历出使用Symbol键值,需要改成Reflect.ownKey():

js 复制代码
const deepClone = (target) => {
    // null或基本数据类型则直接返回
    if(target === null || typeof target !== "object") {
        return target;
    }
    // 若拷贝对象诸如Date, RegExp这些特殊引用类型,则需要创建新的实例返回
    if(target instanceof Date) return new Date(Date);
    if(target instanceof RegExp) return new RegExp(target);
    // 其它引用类型处理...

    // 创建拷贝对象
    const cloneTarget = Array.isArray(target) ? [] : {};

    // 这里等价于: Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
    Reflect.ownKeys(target).forEach(key => {
       // 递归拷贝
       cloneTarget[key] = deepClone(target[key]);
    })
    return cloneTarget;
}

测试:

js 复制代码
const obj = {a: 1};
const key = Symbol("b");
obj[key] = 2;
const newObj = deepClone(obj);
console.log(newObj); // {a: 1, Symbol(b): 2}

成功拷贝了Symbol键

处理循环引用

若对象中的某一个键的值就是这个对象本身,就会形成循环引用:

js 复制代码
const obj = {
    a: 1,
    b: 2,
    c: 3
}
obj.child = obj;

这时使用上面的深拷贝函数,会因为无限递归进入死循环

解决方法是额外开辟一个内存空间存储当前对象和拷贝对象的关系,当需要拷贝当前对象时,先去存储空间中找,若存在过则直接返回,这样就避免了无限递归的场景:

js 复制代码
// 新加一个cacheTarget用于存储当前对象和拷贝对象间的关系
const deepClone = (target, cacheMap = new WeakMap()) => {
    // null或基本数据类型则直接返回
    if(target === null || typeof target !== "object") {
        return target;
    }
    // 若拷贝对象诸如Date, RegExp这些特殊引用类型,则需要创建新的实例返回
    if(target instanceof Date) return new Date(Date);
    if(target instanceof RegExp) return new RegExp(target);
    // 其它引用类型处理...
    
    // 创建拷贝对象前先判断当前对象是否被拷贝过,若存在则直接方法
    if(cacheMap.has(target)) return cacheMap.get(target)
    
    // 创建拷贝对象
    const cloneTarget = Array.isArray(target) ? [] : {};
    // 存储当前对象和拷贝对象进cacheTarget
    cacheMap.set(target, cloneTarget);
    
    // 这里等价于: Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
    Reflect.ownKeys(target).forEach(key => {
       // 递归拷贝, 这里别忘了传入cacheMap
       cloneTarget[key] = deepClone(target[key], cacheMap);
    })
    return cloneTarget;
}

测试:

js 复制代码
const obj = {
    a: 1,
    b: 2,
    c: 3
}
obj.child = obj;

const newObj = deepClone(obj);
console.log(newObj) // {a: 1, b: 2, c: 3, child: {...}}

总结

解决完循环递归,基本已将深拷贝的大部分场景都考虑进去,当然是还有很多缺陷,如对函数的处理。

面试中如果遇到手写深拷贝的场景,能写到这里绝对够了。如果是实际的开发场景,那还需要考虑性能,团队规范等实际情况,这里便不再赘述。

如果文章对您有帮助,您的👍便是对博主最大的鼓励,也欢迎在评论区留下您宝贵的意见

相关推荐
**之火2 分钟前
Web Components 是什么
前端·web components
顾菁寒3 分钟前
WEB第二次作业
前端·css·html
你好龙卷风!!!5 分钟前
vue3 怎么判断数据列是否包某一列名
前端·javascript·vue.js
程序员清风23 分钟前
浅析Web实时通信技术!
java·后端·面试
兔老大的胡萝卜1 小时前
threejs 数字孪生,制作3d炫酷网页
前端·3d
测试19981 小时前
外包干了2年,快要废了。。。
自动化测试·软件测试·python·面试·职场和发展·单元测试·压力测试
shenweihong2 小时前
javascript实现md5算法(支持微信小程序),可分多次计算
javascript·算法·微信小程序
齐 飞2 小时前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
mingzhi612 小时前
渗透测试-快速获取目标中存在的漏洞(小白版)
安全·web安全·面试·职场和发展
巧克力小猫猿2 小时前
基于ant组件库挑选框组件-封装滚动刷新的分页挑选框
前端·javascript·vue.js