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: {...}}

总结

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

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

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

相关推荐
梦境之冢12 分钟前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun15 分钟前
vue VueResource & axios
前端·javascript·vue.js
J总裁的小芒果31 分钟前
THREE.js 入门(六) 纹理、uv坐标
开发语言·javascript·uv
m0_5485147732 分钟前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect32 分钟前
xss csrf怎么预防?
前端·xss·csrf
Calm55035 分钟前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊39 分钟前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_7482398340 分钟前
前端bug调试
前端·bug
m0_7482329242 分钟前
[项目][boost搜索引擎#4] cpp-httplib使用 log.hpp 前端 测试及总结
前端·搜索引擎
新中地GIS开发老师1 小时前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信