JavaScript深拷贝与浅拷贝的区别与实现方法有哪些?

浅拷贝

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

JavaScript深拷贝与浅拷贝的区别与实现方法有哪些?

实现浅拷贝的几种方法:

  1. Object.assign()
let a=[0,1,2,3,4],
let b = Object.assign({}, a) 
  1. ES6的拓展运算符
  2. Object.create
let a=[0,1,2,3,4],
let b =  Object.create(a) 

深拷贝

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

JavaScript深拷贝与浅拷贝的区别与实现方法有哪些?

实现方法

在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。

JSON.parse

JSON.parse(JSON.stringify());

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。

for循环

深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

这是一个最基础版本的深拷贝,但是他还有非常多的缺陷,比如,还没有考虑数组。

考虑数组

在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变:

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

考虑循环引用

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

如果采用之前的方法拷贝这个对象,运行会显示堆栈溢出,原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况。

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

可以选择Map这种数据结构:

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

同时可以用WeakMap替代Map来优化代码。

WeakMap的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。如果我们使用Map的话,那么对象间是存在强引用关系的。

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;

如果是WeakMap的话,target和obj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

其他数据类型

在上面的代码中,我们其实只考虑了普通的object和array两种数据类型,实际上所有的引用类型远远不止这两个,可以使用toString来获取准确的引用类型:

functiongetType(target) {
    returnObject.prototype.toString.call(target);
}

可以抽离出一些常用的数据类型

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型

可继续遍历的类型

上面我们已经考虑的object、array都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有Map,Set等都是可以继续遍历的类型。

有序这几种类型还需要继续进行递归,首先需要获取它们的初始化数据,可以通过拿到constructor的方式来通用的获取。

继续改写clone函数,对可继续遍历的数据类型进行处理:

function clone(target, map = new WeakMap()) {

    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}

不可继续遍历的类型

其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理

Bool、Number、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        default:
            return null;
    }
}

克隆Symbol类型

functioncloneSymbol(targe) {
    returnObject(Symbol.prototype.valueOf.call(targe));
}

克隆正则


function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

克隆函数

实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的。

lodash对函数的处理:

 const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

可见这里如果发现是函数的话就会直接返回了,没有做特殊的处理

咨询方案 获取更多方案详情                        
(0)
研发专家-斑点狗研发专家-斑点狗
上一篇 2024年8月23日 下午3:13
下一篇 2024年9月1日 上午9:17

相关推荐