阅读 200

JS算法-浅拷贝和深拷贝

本文主要内容转自如何写出一个惊艳面试官的深拷贝?

浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

image.png

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

image.png

最简版

JSON.parse(JSON.stringify(obj));
复制代码

这种深拷贝方式存在以下问题:

  • obj里如果有时间对象,时间对象会被JSON.stringify序列化为字符串
  • obj里如果有RegExp、Error对象,将会序列化为空对象
  • obj里有函数和undefined,序列化后,函数和undefined会丢失
  • obj里有NaN、Infinity和-Infinity,序列化后结果为null
  • obj中的对象有构造函数生成的,使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor
  • obj中存在循环引用的情况也无法正确实现深拷贝

基础版

如果是浅拷贝的话,只需要遍历target对象中所有属性,赋值给目标对象。基本类型直接赋值,引用类型赋值内存地址即可,如下:

function shallowClone(target) {
    let clone = {};
    for(let i in target) {
        clone[i] = target[i];
    }
    return clone;
}
复制代码

如果是深拷贝的话,通过递归解决,基本类型直接赋值,引用类型通过for...in遍历赋值

function deepClone(target) {
    if(typeof target === 'object') {
        let clone = {};
        for(let key in target) {
            clone[key] = deepClone(target[key])
        }
        return clone;
    } else {
        return target;
    }
}
复制代码
const target = {
    a: 1,
    b: {
        c: true,
        d: {
            e: 3
        }
    }
}
复制代码

很显然,此方法可以满足基本深拷贝的需求。但是没有考虑包含数组的情况。

考虑数组的深拷贝

function deepClone(target) {
    if (typeof target === 'object') {
        let clone = Array.isArray(target) ? [] : {};
        for(let key in clone) {
            clone[key] = deepClone(target[key]);
        }
        return clone;
    } else {
        return target;
    }
}
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: [
            {
                a: 1
            }
        ]
    },
    field4: [
        { 
            c: { d: 4 } 
        },
        4, 
        8
    ]
};
复制代码

目前实现了对于target对象的深拷贝,包含对象和数组。

循环引用

我们执行下面这样一个测试用例:

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};
target.target = target;
复制代码

可以看到下面的结果:

image.png

很明显,因为递归进入死循环导致栈内存溢出了。

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function clone(target, map = new Map()) {
    if (typeof target === 'object') {
        let clone = Array.isArray ? [] : {}
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, clone)
        for(let key in target) {
            clone[key] = clone(target[key]);
        }
        return clone;
    } else {
        return target;
    }
}
复制代码

其他数据类型

目前只考虑了普通的objectarray数据类型,以及基本数据类型。还需要考虑更多数据类型。最准备的类型判断是通过Object.prototype.toString.call来判断。

image.png

// getType 获得各种数据类型
function getType(target) {
    return Object.prototype.toString.call(target);
}

// isObject 判断target是否为object
function isObject(target) {
    const type = typeof target;
    return type !== null && (type === 'object' || type === 'function');
}

function deepClone(target, map = new Map()) {
    if (!isObject(target)) {
        return target;
    }
    
    const type = getType(target);
    let clone;
    
    // 避免循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, clone);
    
    // clone set
    if (type === '[object Set]') {
        target.forEach(value => {
            clone.add(deepClone(value, map));
        });
        return clone;
    }
    
    // clone map
    if (type === '[object Map]') {
        target.forEach((value, key) => {
            clone.set(key, deepClone(value, map))
        })
        return clone;
    }
    
    // clone 对象和数组
    if (type === ['object Array'] || type === ['object Object']) {
        clone = Array.isArray(target) ? [] : {}
        for(let key in target) {
            clone[key] = deepClone(target[key], map);
        }
    }
    return clone;
}
复制代码
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
};
复制代码

总结

本文只转载了部分内容,只实现了基本数据类型拷贝和object,array,set,map等引用类型的拷贝,以及循环引用拷贝,更多内容请看原文。

参考文章

如何写出一个惊艳面试官的深拷贝?

关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑

文章分类
前端
文章标签