likes
comments
collection
share

[javascript核心-15] 手写完美深拷贝代码实现🍌

作者站长头像
站长
· 阅读数 48

本文github地址:JavaScript_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士


深拷贝与浅拷贝

引用类型的变量赋值指向的内存是同一块,因此需要深拷贝,重新申请内存

一维数组拷贝

可以直接用扩展运算符

let arr1 = [1,3,4]
let arr2 = [...arr1]

拓展运算符并不能解决嵌套引用类型的问题,只能拷贝一层

json对象的简单深拷贝

问题:

1.函数、日期、正则对象都会出现问题(会变为字符串并且变形)

2.不能解决循环引用问题

let obj = {
    name:'flten'
}

let obj2 = let obj = {
    name:'flten'
}

let obj2 = JSON.parse(JSON.stringify(obj))
基本递归深拷贝

注意:for...in 循环包括了继承的属性,不过可以通过hasOwnProperty来进行判断,筛掉原型上拷贝的对象

function deepClone(source){
    const target = source.constructor === Array ? []:{};
    for (const key in source) {
        if (Object.hasOwnProperty.call(source, key)) {
            let element = source[key]
            if(element && typeof element === 'object'){
                target[key] = deepClone(element)
            }else{
                target[key] = source[key]
            }
        }
    }
    return target
}

存在的问题:

1.无法拷贝函数undefinedsymbol

2.日期对象Date等特殊对象的type of判断也是object

3.无法处理循环引用的对象

处理其他类型
function deepClone(source){
    // 非引用类型直接返回
    if(source === null || typeof source !== 'object') return source
    // 单独处理RegExp和Date
    if(source instanceof RegExp) return new RegExp(source)
    if(source instanceof Date) return new Date(source)
    if(source instanceof Set) {
        const newSet = new Set()
        for(const item of source){
            newSet.add(deepClone(item))
        }
        return newSet
    }
    if(typeof source === 'function') return source

    // 克隆的对象和之前的结果保持一样的所属类
    const target = new source.constructor;
    for (const key in source) {
        if (Object.hasOwnProperty.call(source, key)) {
            let element = source[key]
            target[key] = deepClone(element)
        }
    }
    return target
}

问题:

  1. 没有解决循环引用问题

  2. 其他特殊类型要一一处理判断

  3. 对于Symbol,作为属性的值时,它的typeof结果为symbol,没有被考虑到

  4. 对于Symbol,作为属性时,它不会被for...in遍历出来,也没有被考虑到

考虑Symbol作为属性值及属性的情况
function deepClone(source){
    // 对值是 Symbol 类型的属性进行判断
    if(typeof source === 'symbol') {
        return new Symbol(source.description)
    }
    // 非引用类型直接返回
    if(source === null || typeof source !== 'object') return source
    // 单独处理RegExp和Date
    if(source instanceof RegExp) return new RegExp(source)
    if(source instanceof Date) return new Date(source)
    if(source instanceof Set) {
        const newSet = new Set()
        for(const item of source){
            newSet.add(deepClone(item))
        }
        return newSet
    }
    if(typeof source === 'function') return source

    // 克隆的对象和之前的结果保持一样的所属类
    const target = new source.constructor;
    for (const key in source) {
        if (Object.hasOwnProperty.call(source, key)) {
            let element = source[key]
            target[key] = deepClone(element)
        }
    }
    // 处理 Symbol 作为对象属性的情况
    const symbolKeys = Object.getOwnPropertySymbols(source)
    for (const symbolKey of symbolKeys) {
        target[Symbol(symbolKey.description)] = deepClone(source[symbolKey])
    }
    return target
}

问题:

  1. 没有解决循环引用问题

  2. 其他特殊类型要一一处理判断

考虑循环引用问题

循环引用如果在深拷贝时不处理会造成栈溢出。

let map = new Map()
function deepClone(source){
    // 对值是 Symbol 类型的属性进行判断
    if(typeof source === 'symbol') {
        return new Symbol(source.description)
    }
    // 判断循环引用
    if(map.get(source)){
        return map.get(source)
    }
    // 非引用类型直接返回
    if(source === null || typeof source !== 'object') return source
    // 单独处理RegExp和Date
    if(source instanceof RegExp) return new RegExp(source)
    if(source instanceof Date) return new Date(source)
    if(source instanceof Set) {
        const newSet = new Set()
        for(const item of source){
            newSet.add(deepClone(item))
        }
        return newSet
    }
    if(typeof source === 'function') return source

    // 克隆的对象和之前的结果保持一样的所属类
    const target = new source.constructor;
    // 保持当前对象为key,为判断循环引用做准备
    map.set(source,target)
    for (const key in source) {
        if (Object.hasOwnProperty.call(source, key)) {
            let element = source[key]
            target[key] = deepClone(element)
        }
    }
    // 处理 Symbol 作为对象属性的情况
    const symbolKeys = Object.getOwnPropertySymbols(source)
    for (const symbolKey of symbolKeys) {
        target[Symbol(symbolKey.description)] = deepClone(source[symbolKey])
    }
    return target
}

问题:

  1. 增加了额外的map,并且对原对象形成了强引用

  2. 增加了全局变量

  3. 其他特殊类型要一一处理判断


function deepClone(source, map = new WeakMap()){
    // 对值是 Symbol 类型的属性进行判断
    if(typeof source === 'symbol') {
        return new Symbol(source.description)
    }
    // 判断循环引用
    if(map.get(source)){
        return map.get(source)
    }
    // 非引用类型直接返回
    if(source === null || typeof source !== 'object') return source
    // 单独处理RegExp和Date
    if(source instanceof RegExp) return new RegExp(source)
    if(source instanceof Date) return new Date(source)
    if(source instanceof Set) {
        const newSet = new Set()
        for(const item of source){
            newSet.add(deepClone(item))
        }
        return newSet
    }
    if(typeof source === 'function') return source

    // 克隆的对象和之前的结果保持一样的所属类
    const target = new source.constructor;
    // 保持当前对象为key,为判断循环引用做准备
    map.set(source,target)
    for (const key in source) {
        if (Object.hasOwnProperty.call(source, key)) {
            let element = source[key]
            target[key] = deepClone(element, map)
        }
    }
    // 处理 Symbol 作为对象属性的情况
    const symbolKeys = Object.getOwnPropertySymbols(source)
    for (const symbolKey of symbolKeys) {
        target[Symbol(symbolKey.description)] = deepClone(source[symbolKey], map)
    }
    return target
}

核心:递归调用时,将map传入,这样每一次递归调用使用的都是同一个map.这样在函数执行结束时,map会作为参数随着函数一起被销毁。

问题:

其他特殊类型要一一处理判断

完美深拷贝

  1. 利用 WeekMap() 的键对自己所引用对象的引用都是弱引用的特性,在没有其他引用和该键引用同一对象的情况下,这个对象将会被垃圾回收
  2. 为了解决循环引用的问题,设置一个哈希表存储已拷贝过的对象进行循环检测,当检测到当前对象已存在于哈希表中时,取出该值并返回即可
  3. 查哈希表,防止循环拷贝。如果成环了(对象循环引用),参数obj = obj.loop = 最初的obj,则会在WeakMap中找到第一次放入的obj提前返回第一次放入WeakMap的cloneObj,解决对象循环引用的问题
  4. 如果参数为Date, RegExp, Set, Map, WeakMap, WeakSet等引用类型,则直接生成一个新的实例
  5. 使用Object.getOwnPropertyDescriptors遍历传入参数所有属性描述符
  6. 使用Object.getOwnPropertySymbols获取所有 Symbol 类型键
  7. 使用Reflect.ownKeys(obj)拷贝不可枚举属性和符号类型
//判断是否为复杂数据类型
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null);

const deepClone = function(obj,hash = new WeakMap()){
    if(hash.has(obj)) return hash.get(obj);

    //如果参数为Date, RegExp, Set, Map, WeakMap, WeakSet等引用类型,则直接生成一个新的实例
    let type = [Date,RegExp,Set,Map,WeakMap,WeakSet];
    if(type.includes(obj.constructor)) return new obj.constructor(obj);

    //遍历传入参数所有属性描述符
    let allDesc = Object.getOwnPropertyDescriptors(obj);
    //继承原型
    let cloneObj = Object.create(Object.getPrototypeOf(obj),allDesc);

    // 获取所有 Symbol 类型键
    let symKeys = Object.getOwnPropertySymbols(obj);
    // 拷贝 Symbol 类型键对应的属性
    if (symKeys.length > 0) {
        symKeys.forEach(symKey => {
            cloneObj[symKey] = isComplexDataType(obj[symKey]) ? deepClone(obj[symKey], hash) : obj[symKey]
        })
    }

    // 哈希表设值
    hash.set(obj,cloneObj);

    //Reflect.ownKeys(obj)拷贝不可枚举属性和符号类型
    for(let key of Reflect.ownKeys(obj)){
        // 如果值是引用类型并且非函数则递归调用deepClone
        cloneObj[key] =
            (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key],hash) : obj[key];
    }
    return cloneObj;
};

let obj = {
    arr : [0,1,2,3,4,5,6]
};

测试:

let obj2 = deepClone(obj);
obj2.str = 'flten';
console.log(obj,obj2);//{arr: [0, 1, 2, 3, 4, 5, 6],str: "flten"}

console.log('-------------');

//处理循环引用测试
let a = {
    name: "lk",
    course: {
        vue: "Vue.js",
        react: "React.js"
    },
    a1: undefined,
    a2: null,
    a3: 123,
    a4: NaN
};

//对象循环引用
a.circleRef = a;

let b = deepClone(a);
console.log(b);
// {
// 	name: "lk",
// 	a1: undefined,
//	a2: null,
// 	a3: 123,
//	a4: NaN,
// 	course: {vue: "Vue.js",react: "React.js"},
// 	circleRef: {name: "lk",a1: undefined, a2: null, a3: 123, a4:NaN …}
// }

本文github地址:JavaScript_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士

转载自:https://juejin.cn/post/7247278895338913851
评论
请登录