深拷贝实现最完整方案

本文探讨了JavaScript中的深拷贝与浅拷贝概念,分析了常见深拷贝实现的局限性,如对象字段值为null、空对象、数组及循环引用等问题,并逐步优化解决方案。通过JSON.stringify()的限制及Object.prototype.toString.call()识别数据类型,提供了更全面的深拷贝实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先说说深拷贝和浅拷贝

  • 浅拷贝

所谓浅拷贝,就是只复制最外一层,里面的都还是相同引用

// 浅拷贝
const a = { name: 'xiaoming', age: 23 }
const b = {}
for (let key in a){
  b[key] = a[key]
}

console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false
console.log(b.name === a.name) // true

  • 深拷贝

深拷贝,是将一个对象拷贝到另一个新变量,这个新变量指向的是一块新的堆内存地址

// 深拷贝

function cloneDeep(target) {
    // ...dosomething   实现深拷贝
}

const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)

console.log(b) // { name: 'xiaoming', age: 23}
console.log(b === a) // false
console.log(b.name === a.name) // false

  • 深拷贝实现代码

常用版本

function cloneDeep(target) {
    return JSON.parse(JSON.stringify(target))
}

const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)

console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false

虽然大多数时候这么使用是没问题的,但这种方式在工作中还是有很多问题

    1、对象中有字段值为undefined,转换后则会直接字段消失
    2、对象如果有字段值为RegExp对象,转换后则字段值会变成{}
    3、对象如果有字段值为NaN、+-Infinity,转换后则字段值变成null
    4、对象如果有环引用,转换直接报错

    升级一下

function cloneDeep(target) {
    const temp = {}
    for (const key in target) {
        temp[key] = target[key]
    }
    return temp
}

const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)

console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false

 看起来不错了,但是遇到层数比较深的还是不可以,在优化一下,递归一下。

function cloneDeep(target) {
    // 基本数据类型直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用数据类型特殊处理
    const temp = {}
    for (const key in target) {
        // 递归
        temp[key] = cloneDeep(target[key])
    }
    return temp
}

const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' }
}
const b = cloneDeep(a)

console.log(b === a) // false

 但是现在只可以是拷贝对象,数组还不行,在优化一下。

function cloneDeep(target) {
    // 基本数据类型直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用数据类型特殊处理
    // 判断数组还是对象
    const temp = Array.isArray(target) ? [] : {}
    for (const key in target) {
        // 递归
        temp[key] = cloneDeep(target[key])
    }
    return temp
}

const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' },
    works: ['2020', '2021']
}
const b = cloneDeep(a)

console.log(b === a) // false

 但是并没有解决一个重要的问题,环引用的问题。

  • 什么是循环引用

当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用,(当然不止这一种情况,不过原理是一样的)下面通过代码来说明一下。

  let obj1 = {};
  let obj2 = {
         b: obj1
      };
  obj1.a = obj2;

JSON.tostringify 无法将一个无限引用的对象序列化为 JOSN 字符串。

下面是JSON.stringify() 将值转换为相应的JSON格式的解释:

  • 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
  • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  • Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。

当然解决上述的方法也可以这么写

  let obj1 = {
  };
  let obj2 = {
    b: obj1
  };
  obj1.a = obj2;
  let c = JSON.decycle(obj1);
  JSON.stringify(c)​

结合之上的问题,在升级一下深拷贝

function cloneDeep(target, map = new Map()) {
    // 基本数据类型直接返回
    if (typeof target !== 'object') {
        return target
    }
    // 引用数据类型特殊处理
    // 判断数组还是对象
    const temp = Array.isArray(target) ? [] : {}
    if (map.get(target)) {
        // 已存在则直接返回
        return map.get(target)
    }
   // 不存在则第一次设置
    map.set(target, temp)

    for (const key in target) {
        // 递归
        temp[key] = cloneDeep(target[key], map)
    }
    return temp
}

const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' },
    works: ['2020', '2021']
}
a.key = a // 环引用
const b = cloneDeep(a)

console.log(b === a) // false

看起来可以了,但是并不能实现所有的需求,只能实现一些比较常见的数组、对象等等 。

但其实,引用数据类型可不止只有数组和对象,我们还得解决以下的引用类型的拷贝问题,那怎么判断每个引用数据类型的各自类型呢?可以使用Object.prototype.toString.call()

这个会返回所有的数据类型如下;

类型
MapObject.prototype.toString.call(new Map())[object Map]
SetObject.prototype.toString.call(new Set())[object Set]
ArrayObject.prototype.toString.call([])[object Array]
ObjectObject.prototype.toString.call({})[object Object]
SymbolObject.prototype.toString.call(Symbol())[object Symbol]
RegExpObject.prototype.toString.call(new RegExp())[object RegExp]
FunctionObject.prototype.toString.call(function() {})[object Function]

把上树类型分成两类,第一类为可遍历,第二类为不可遍历。

// 可遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

// 不可遍历类型
const symbolTag = '[object Symbol]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

// 将可遍历类型存在一个数组里
const canForArr = ['[object Map]', '[object Set]','[object Array]', '[object Object]']

// 将不可遍历类型存在一个数组
const noForArr = ['[object Symbol]', '[object RegExp]', '[object Function]']

// 判断类型的函数
function checkType(target) {
    return Object.prototype.toString.call(target)
}
// 判断引用类型的temp
function checkTemp(target) {
    const c = target.constructor
    return new c()
}

 判断两种的方法总结一下

// 拷贝Function的方法
function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

// 拷贝Symbol的方法
function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

// 拷贝RegExp的方法
function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

 结合一下 最终版本

function cloneDeep(target, map = new Map()) {
    // 获取类型
    const type = checkType(target)
    // 基本数据类型直接返回
    if (!canForArr.concat(noForArr).includes(type)) return target
    // 判断Function,RegExp,Symbol
    if (type === funcTag) return cloneFunction(target)
    if (type === regexpTag) return cloneReg(target)
    if (type === symbolTag) return cloneSymbol(target)

    // 引用数据类型特殊处理
    const temp = checkTemp(target)

    if (map.get(target)) {
        // 已存在则直接返回
        return map.get(target)
    }
    // 不存在则第一次设置
    map.set(target, temp)

    // 处理Map类型
    if (type === mapTag) {
        target.forEach((value, key) => {
            temp.set(key, cloneDeep(value, map))
        })

        return temp
    }

    // 处理Set类型
    if (type === setTag) {
        target.forEach(value => {
            temp.add(cloneDeep(value, map))
        })

        return temp
    }

    // 处理数据和对象
    for (const key in target) {
        // 递归
        temp[key] = cloneDeep(target[key], map)
    }
    return temp
}


const a = {
    name: 'xiaoming',
    age: 23,
    hobbies: { sports: 'swim', game: 'lol' },
    works: ['2020', '2021']
    map: new Map([['aaa', 0001], ['bbb', 0002]]),
    set: new Set([1, 2, 3]),
    func: (do) => `${do}!`,
    sym: Symbol(1111),
    reg: new RegExp(/oooooooooo/g),
}
a.key = a // 环引用

const b = cloneDeep(a)
console.log(b)
console.log(b === a) // false

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值