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)

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

相关推荐
黑客老陈29 分钟前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安34 分钟前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
暴富的Tdy1 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se1 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235611 小时前
web 渗透学习指南——初学者防入狱篇
前端
℘团子এ1 小时前
js和html中,将Excel文件渲染在页面上
javascript·html·excel
z千鑫1 小时前
【前端】入门指南:Vue中使用Node.js进行数据库CRUD操作的详细步骤
前端·vue.js·node.js
m0_748250742 小时前
Web入门常用标签、属性、属性值
前端