JavaScript深拷贝的实现

332 阅读7分钟

拷贝变量在我们日常编写代码中随处可见,今天就来聊一聊拷贝。拷贝分为两种:浅拷贝深拷贝,那么什么是浅拷贝,什么是深拷贝呢?

深拷贝和浅拷贝的定义

在介绍深拷贝和浅拷贝之前,我们可以把一个变量结构想象成一棵树,对于简单类型的数据比如number string等就是只有一层的树,而对于复杂类型的数据比如Array Object等就有可能是很多层的树了。有了这层概念,我们接下来说明浅拷贝和深拷贝的定义

  • 浅拷贝: 只拷贝一层,即拷贝深度deep = 1;比如:
const arr = [1, 2, {name: '浅拷贝'}];
const arrCopy = [];
arr.forEach((item) => {
    arrCopy.push(item);
})

这里我想根据自己的理解说明一下: 对于简单类型赋值操作可以看作是浅拷贝,但是对于复杂类型赋值操作就不能看作是浅拷贝了

  • 深拷贝:对被拷贝对象的每一层进行拷贝;比如:
const arr = [1, 2, [3, 4]];
const arrCopy = [];
arr.forEach((item) => {
    if (Array.isArray(item)) {
        const deepArr = [];
        item.forEach((itemC) => {
            deepArr.push(itemC);
        });
        arrCopy.push(deepArr);
    } else {
        arrCopy.push(item);
    }
})

从上面我们可以看出浅拷贝对于复杂结构类型带来的副作用,这里就不多说了。下面我们介绍怎么实现一个深拷贝。

深拷贝方法1:JSON.parse(JSON.stringify(param))

对于利用JSON.parse和JSON.stringify进行深拷贝,我想这是大家在日常编写代码中最常用也是最方便的深拷贝方式。比如:

const obj = {
    a: 1,
    b: {bb: 2}
}
const objCopy = JSON.parse(JSON.stringify(obj));
// 这里我们修改obj中b属性值
obj.b.bb = 3;
console.log(objCopy) // 输出{a: 1, b: {bb: 2}}

利用这中方式进行深拷贝可以满足我们平常大多数情况了,但是会有一下的问题,我们通过下面例子看一下

const obj = {
    a: /a/,
    b: function(){console.log('I am Function')}
}
console.log(JSON.parse(JSON.stringify(obj)))
// 输出结果为:{a: {}}

通过上面结果我们可以看出对于正则表达式函数,利用该方法进行深拷贝时是有问题的。所以以后在编写代码的过程中需要注意一下。
既然这种方法有缺陷,那么我们就自己动手编写一个自己的深拷贝方法,这也是这边文章的重点。

DIY 深拷贝1

通过前面深拷贝的定义,如果想进行深拷贝想到的就是使用递归了。那话不多说直接上代码:

/**
 * @param {*} param 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
function deepCopy1(param) {
    // 第一步判断是否是简单类型
    // 如果是简单类型就直接返回了
    if (param === null || typeof param !== 'object') {
        return param
    }
    // 保存拷贝的值
    const copyObj = {};
    // 利用深度遍历循环递归对象每一层的每个属性值
    for(let key in param) {
        copyObj[key] = deepCopy(param[key]);
    }
    return copyObj;
}

至此第一个版本就已经实现了,还是很简单的。细心的童鞋会发现这种方式存在局限性,就是只针对了对象类型Object。但是复杂类型不止是对象,还有一个特别常见的数组;那么下面第二个版本就将数组考虑进去。

DIY 深拷贝2

/**
 * @param {*} param 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
function deepCopy2(param) {
    // 第一步判断是否是简单类型
    // 如果是简单类型就直接返回了
    if (param === null || typeof param !== 'object') {
        return param
    }
    // 保存拷贝的值
    const copyObj = Array.isArray(param) ? [] : {};
    // 利用深度遍历循环递归对象每一层的每个属性值
    for(let key in param) {
        copyObj[key] = deepCopy(param[key]);
    }
    return copyObj;
}

对比第一个版本,只是在初始化对象的时候加了数组的考虑,即:

const copyObj = Array.isArray(param) ? [] : {};

是不是so easy。看到这,是不是觉得深拷贝其实也不是很复杂。但是我们还需要考虑一个问题就是循环调用,如下的情况:

const obj = {a: 'a'};
obj[b] = obj;
console.log(deepCopy2(obj));
// 报错:Uncaught RangeError: Maximum call stack size exceeded

报错的大致意思就是内存溢出了,为什么会出现这样的错,因为对象自己调用自己了,这种情况函数就会一直递归遍历,不会出结果。那么怎么修改这种问题呢?下面我们就来实现第三个版本。

DIY 深拷贝3

为了避免上面这种错误,做法是当属性值之前被遍历过了我们就不进行遍历,而是直接返回,这样就不会因为自己调用自己而出现栈溢出的情况了。有了思路我们下面需要考虑的是怎么判断当前的属性值我们之前遍历过呢,这个我们可以使用容器把之前遍历过的值存下来,每次对属性值操作之前先判断该值在容器中有没有,到这问题就剩这个容器是个什么类型的数据,在这里就直接告诉大家了,使用的是WeakMap,至于为什么不用ObjectMap感兴趣的同学可以自己探究一下。

const weakMap = new WeakMap();
/**
 * @param {*} param 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
function deepCopy3(param) {
    // 第一步判断是否是简单类型
    // 如果是简单类型就直接返回了
    if (param === null || typeof param !== 'object') {
        return param
    }
    // 保存拷贝的值
    const copyObj = Array.isArray(param) ? [] : {};
    // 判断属性值之前有没有被遍历过
    if (weakMap.has(param)) {
        return weakMap.get(param)
    }
    weakMap.set(param, copyObj);
    // 利用深度遍历循环递归对象每一层的每个属性值
    for(let key in param) {
        copyObj[key] = deepCopy(param[key]);
    }
    return copyObj;
}

写到这,基本上就没什么问题了,但是既然我们用到了WeakMap类型的数据我们是不是也得考虑将ES6新增的几个复杂类型的数据考虑进去,下面我们就来实现最后一个版本(真的是最后一个了)

DIY 深拷贝终极版

在ES6中新增了几个复杂类型的数据结构: SetWeakSetMapWeakMap,下面我们就将这几种类型考虑进去。由于现在类型变多了,我们就不能像上面一样简单进行数据判断了。首先编写一个判断数据类型的函数:

/**
 * 获取数据类型
 * @param {*} data 待获取数据类型的数据
 * @return {string} 对应的数据类型
 **/
 function getDataType(data) {
    const type = Object.prototype.toString.call(type);
    return type.slice(8, type.length - 1);
 }
const weakMap = new WeakMap();
/**
 * @param {*} param 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
function deepCopy3(param) {
    // 第一步判断是否是简单类型
    // 如果是简单类型就直接返回了
    if (param === null || typeof param !== 'object') {
        return param
    }
    // 获取数据类型
    const dataType = getDataType(param);
    let copyObj;
    switch(dataTpye) {
        case 'Object':
            copyObj = {};
            break;
        case 'Array':
            copyObj = [];
            break;
        case 'Set':
            copyObj = new Set();
            break;
        case 'WeakSet':
            copyObj = new WeakSet();
            break;
        case 'Map':
            copyObj = new Map();
            break;
        case 'WeakMap':
           copyObj = new WeakMap();
           break;
        default:
           copyObj = {};
    }
    // 判断属性值之前有没有被遍历过
    if (weakMap.has(param)) {
        return weakMap.get(param)
    }
    weakMap.set(param, copyObj);
    // 利用深度遍历循环递归对象每一层的每个属性值
    if (['Set', 'WeakSet', 'Map', 'WeakMap'].includes(dataTpye)) {
        param.forEach((value, key) => {
            copyObj.set(key, deepCopy(value));
        })
    } else {
        for(let key in param) {
            copyObj[key] = deepCopy(param[key]);
        }
    }
    return copyObj;
}

至此我们就把能想到的复杂类型的数据结构都考虑进去了。 不知道大家是不是觉得上面根据类型去初始化值这一块特别的繁琐,那下面我们就行优化一下:

const weakMap = new WeakMap();
/**
 * @param {*} param 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
function deepCopy3(param) {
    // 第一步判断是否是简单类型
    // 如果是简单类型就直接返回了
    if (param === null || typeof param !== 'object') {
        return param
    }
    // 获取数据类型
    const dataType = getDataType(param);
    const copyObj = new window[dataType]();
    // 判断属性值之前有没有被遍历过
    if (weakMap.has(param)) {
        return weakMap.get(param)
    }
    weakMap.set(param, copyObj);
    // 利用深度遍历循环递归对象每一层的每个属性值
    if (['Set', 'WeakSet', 'Map', 'WeakMap'].includes(dataTpye)) {
        param.forEach((value, key) => {
            copyObj.set(key, deepCopy(value));
        })
    } else {
        for(let key in param) {
            copyObj[key] = deepCopy(param[key]);
        }
    }
    return copyObj;
}

至此深拷贝已经全部完成。希望大家可以帮助大家