JS手写实现深拷贝

手写深拷贝

一、通过JSON.stringify

JSON.parse(JSON.stringify(obj))是比较常用的深拷贝方法之一

原理:利用JSON.stringify 将JavaScript对象序列化成为JSON字符串,并将对象里面的内容转换成字符串,再使用JSON.parse来反序列化,将字符串生成一个新的JavaScript对象

这个方法是目前使用最多的深拷贝的方法,也是最简单的方法,使用示例:

javascript 复制代码
let obj1 = {  
  a: 0,
  b: {
    c: 0
  }
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}

这个方法虽然简单粗暴,但也存在一些问题,在使用该方法时需要注意:

拷贝的对象中如果有 function、undefined、symbol,当使用过JSON.stringify()进行处理之后,都会消失。

javascript 复制代码
const originObj = {
    name: 'test',
    age: undefined,
    func: function () {
        console.log('Hello World');
    },
    key: Symbol('一个独一无二的key')
}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // 只剩下 {name: "test"}
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 Date 引用类型会变成字符串;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有NaN、Infinity以及 -Infinity,JSON 序列化的结果会变成null;
  • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

二、函数库lodash

该函数库也有提供_.cloneDeep用来做深拷贝,可以直接引入并使用:

javascript 复制代码
var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false

三、递归实现深拷贝

实现深拷贝的思路就是,使用for in来遍历传入参数的属性值

  • 如果值是基本类型就直接复制
  • 如果是引用类型就进行递归调用该函数

基础递归

javascript 复制代码
function deepClone (source) {
    //判断source是不是对象
    if (source instanceof Object === false) return source;
​
    //根据source类型初始化结果变量
    let target = Array.isArray(source) ? [] : {};
​
    for (let i in source) {
        // 判断是否是自身属性
        if (source.hasOwnProperty(i)) {
            //判断数据i的类型
            if (typeof source[i] === 'object') {
                target[i] = deepClone(source[i]);
            } else {
                target[i] = source[i];
            }
        }
    }
    return target;
}
​
const obj = {
    info: { c: { d: 1 } },
    age: undefined,
    func: function () {
        console.log('Hello World');
    },
    key: Symbol('一个独一无二的key')
}
​
const resultA = deepClone(obj)
​
obj.info.c.d = 3
console.log(111111, obj); 
// { info: { c: { d: 3 } }, age: undefined, func: f, key: xxxxx}
​
console.log(222222, resultA); 
// { info: { c: { d: 1 } }, age: undefined, func: f, key: xxxxx}
​
​
let resultB = [1, [2, 3], [4, [5]]]
let resultC = deepClone(resultB)
​
resultB[1][1] = 7
console.log(333333, resultB);  // [1, [2, 7], [4, [5]]]
​
console.log(444444, resultC);  // [1, [2, 3], [4, [5]]]

这只是粗略的版本,这样虽然实现了深拷贝,但也存在一些问题:

  • 存在环引用问题(存在循环引用,拷贝会直接爆栈)
  • 对于Date、RegExp、Set、Map等引用类型不能正确拷贝

升级版递归---解决环引用爆栈问题

javascript 复制代码
function deepClone (source, map = new Map()) {
    //判断source是不是对象
    if (source instanceof Object === false) return source;
​
    //根据source类型初始化结果变量
    let target = Array.isArray(source) ? [] : {};
​
    /* ----------------新增---------------- */
    if (map.get(source)) {
        // 已存在则直接返回
        return map.get(source)
    }
    // 不存在则第一次设置
    map.set(source, target)
    /* ----------------新增---------------- */
​
    for (let i in source) {
        // 判断是否是自身属性
        if (source.hasOwnProperty(i)) {
            //判断数据i的类型
            if (typeof source[i] === 'object') {
                // 传递map
                target[i] = deepClone(source[i], map);
            } else {
                target[i] = source[i];
            }
        }
    }
    return target;
}
​
const obj = {
    info: { c: { d: 1 } },
    age: undefined,
    func: function () {
        console.log('Hello World')
    },
    key: Symbol('一个独一无二的key'),
}
​
// 形成环引用
obj.loop = obj
console.log(obj);
​
const resultA = deepClone(obj)
console.log(resultA); // 拷贝成功

最终版递归---解决其余类型拷贝结果

javascript 复制代码
// 可遍历类型
const arrTag = '[object Array]';
const objTag = '[object Object]';
const mapTag = '[object Map]';
const setTag = '[object Set]';
const argTag = '[object Arguments]';
const strTag = '[object String]';
​
// 不可遍历类型
const boolTag = '[object Boolean]';
const numTag = '[object Number]';
const dateTag = '[object Date]';
const errTag = '[object Error]';
const regexpTag = '[object RegExp]';
const symbolTag = '[object Symbol]';
const funTag = '[object Function]';
​
// 将可遍历类型做个集合
const traversalArr = [arrTag, objTag, mapTag, setTag, argTag, strTag];
​
​
// 判断类型的函数(采用最全且无遗漏的判断方式)
function checkType (source) {
    return Object.prototype.toString.call(source)
}
​
// 拷贝RegExp的方法
function cloneReg (source) {
    const reFlags = /\w*$/;
    const result = new source.constructor(source.source, reFlags.exec(source));
    result.lastIndex = source.lastIndex;
    return result;
}
​
// 拷贝Date的方法
function cloneDate (source) {
    return new source.constructor(source.valueOf())
}
​
​
function deepClone (source, map = new Map()) {
    // 非对象直接返回
    if (source instanceof Object === false) return source
​
    // 根据source类型初始化结果变量
    let target = Array.isArray(source) ? [] : {};
​
​
    /* ----------------处理环引用问题---------------- */
    // 已存在则直接返回(仅仅在环引用之间生效)
    if (map.get(source)) return map.get(source)
​
    // 不存在则第一次设置
    map.set(source, target)
    /* ----------------处理环引用问题---------------- */
​
​
    /* ----------------处理Map、Set、Date、RegExp深拷贝失效问题---------------- */
    const type = checkType(source)
​
    console.log(type);
    let emptyObj
​
    // 如果是可遍历类型,直接创建空对象
    if (traversalArr.includes(type)) {
        emptyObj = new source.constructor()
    }
​
    // 处理Map类型
    if (type === mapTag) {
        source.forEach((value, key) => {
            emptyObj.set(key, deepClone(value, map))
        })
        return emptyObj
    }
​
    // 处理Set类型
    if (type === setTag) {
        source.forEach(value => {
            emptyObj.add(deepClone(value, map))
        })
        return emptyObj
    }
​
    // 处理Date类型
    if (type === dateTag) return cloneDate(source)
​
    // 处理Reg类型
    if (type === regexpTag) return cloneReg(source)
    /* ----------------处理Map、Set、Date、RegExp深拷贝失效问题--------------- */
​
​
    for (let item in source) {
        // 判断是否是自身属性
        if (source.hasOwnProperty(item)) {
            // 判断数据i的类型
            // if (source[item] instanceof Object) {
            if (typeof source[item] === 'obejct') {
                target[item] = deepClone(source[item], map);
            } else {
                target[item] = source[item];
            }
        }
    }
    return target;
}
​
const obj = {
    // 基本类型
    str: 'test',
    num: 18,
    boolean: true,
    sym: Symbol('独一无二key'),
​
    // 引用类型(以下8种数据对象均需进行真正意义上的深拷贝)
    obj_object: { name: 'squirrel' },
    arr: [123, '456'],
    func: (name, age) => console.log(`姓名:${name},年龄:${age}岁`),
​
    map: new Map([['t', 100], ['s', 200]]),
    set: new Set([1, 2, 3]),
    date: new Date(),
    reg: new RegExp(/test/g),
}
​
// 形成环引用
obj.loop = obj
​
const result = deepClone(obj)
console.log('手写deepClone结果:', result)

结果如下(完美解决了基础递归中的问题):

相关推荐
慢慢雨夜8 分钟前
uniapp 苹果安全域适配
java·前端·uni-app
凄凄迷人12 分钟前
前端基于Rust实现的Wasm进行图片压缩的技术文档
前端·rust·wasm·图片压缩
敲代码不忘补水14 分钟前
二十种编程语言庆祝中秋节
java·javascript·python·golang·html
我码玄黄21 分钟前
JS 的行为设计模式:策略、观察者与命令模式
javascript·设计模式·命令模式
史努比的大头26 分钟前
前端开发深入了解性能优化
前端
码农研究僧27 分钟前
Java或者前端 实现中文排序(调API的Demo)
java·前端·localecompare·中文排序·collator
营赢盈英34 分钟前
OpenAI API key not working in my React App
javascript·ai·openai·reactjs·chatbot
吕永强1 小时前
HTML表单标签
前端·html·表单标签
范特西是只猫1 小时前
echarts map地图动态下钻,自定义标注,自定义tooltip弹窗【完整demo版本】
前端·javascript·echarts