JavaScript 手撕深拷贝

前端2 min
#JavaScript
目录(7)

深拷贝

JS 虽然没有指针,但是存在引用类型,除了基本数据类型 number、string、boolean、null、undefined、symbol、bigint,其他都是引用类型。常见的引用类型包括: object、array、function、map、set、date、regexp 等。

深拷贝主要考校的是对于引用类型的拷贝,对于基本数据类型,直接赋值即可。

因此深拷贝的实现一般需要考虑这几个方面:

  1. 基本数据类型。

  2. 引用类型的。

    2.1 object、array、function

    2.2 map、set

    2.3 date、regexp

  3. 循环引用。

除此之外还有一些进阶选项:

  1. symbol 作为 key 时的支持。
  2. 原型拷贝。
  3. 不可枚举属性属性拷贝。
  4. 函数的拷贝。
  5. 二进制数组,如 Int8Array、Uint8Array、Uint8ClampedArray 等

实现代码

本代码中未对『进阶选项』进行支持

function deepCopy(data) {
    // 定义 Map 防止循环引用
    const weakMap = new WeakMap();

    // 定义 func 判断值的类型
    function type(value) {
        return value === null ? 'Null' : value === undefined ? 'Undefined' : Object.prototype.toString.call(value).slice(8, -1);
    }

    function clone(value) {
        // 实际 copy 方法
        const copy = function copy(copiedValue) {
            if (weakMap.has(value)) { 
                return weakMap.get(value);
            }
            weakMap.set(value, copiedValue);
            if (value instanceof Map) {
                value.forEach((el, key) => {
                    copiedValue.set(key, clone(el));
                });
            } else if (value instanceof Set) {
                value.forEach(el => {
                    copiedValue.add(clone(el));
                });
            } else {
                for (const key in value) {
                    if(bject.prototype.hasOwnProperty.call(value,key)) {
                        copiedValue[key] = clone(value[key]);
                    }
                }
            }
            return copiedValue;
        };

        // 判断值的类型
        switch (type(value)) {
            case 'Object': return copy(Object.create(Object.getPrototypeOf(value)));
            case 'Array': return copy([]);
            case 'RegExp':
            case 'Date': return new value.constructor(value);
            case 'Map': return copy(new Map());
            case 'Set': return new Set(value);
            default: return value;
        }
    }

    return clone(data);
}

测试对象

// 测试的obj对象
const obj = {
    // =========== 1.基础数据类型 ===========
    num: 0, // number
    str: '', // string
    bool: true, // boolean
    unf: undefined, // undefined
    nul: null, // null
    sym: Symbol('sym'), // symbol
    bign: BigInt(1n), // bigint

    // =========== 2.Object类型 ===========
    // 普通对象
    obj: {
        name: '我是一个对象',
        id: 1
    },
    // 数组
    arr: [0, 1, 2],
    // 函数
    func: function () {
        console.log('我是一个函数')
    },
    // 日期
    date: new Date(0),
    // 正则
    reg: new RegExp('/我是一个正则/ig'),
    // Map
    map: new Map().set('mapKey', 1),
    // Set
    set: new Set().add('set'),
    // =========== 3.其他 ===========
    [Symbol('1')]: 1  // Symbol作为key
};

// 4.添加不可枚举属性
Object.defineProperty(obj, 'innumerable', {
    enumerable: false,
    value: '不可枚举属性'
});

// 5.设置原型对象
Object.setPrototypeOf(obj, {
    proto: 'proto'
})

// 6.设置loop成循环引用的属性
obj.loop = obj

实现思路

懒人版思路

 JSON.parse(JSON.stringify(obj))

通过 JSON 转换一次即可实现深拷贝,但是实际使用中,有很多情况无法处理,酌情处理。

手撕思路

  1. 基本类型,基本类型不涉及引用,直接赋值即可。

  2. function,没想到 function 如何产生变更(不认为其有需要深拷贝的场景),其实也可以直接返回。不放心可以采用下面的方式生成新函数。

    new Function('return' + func.toString())()

  3. array 数组如果仅涉及基本数据类型,进行解构赋值即可。

    [ ...array ]

  4. map、set 遍历 new 一个新的进行赋值。

  5. object 对象,也可以创建一个新的对象,遍历属性进行重新赋值,遍历会遇到以下问题。

    • 创建对象的需要原型保持一致

      Object.create(Object.getPrototypeOf(obj))

    • 遍历对象时,会遍历到原型链上的属性,需要过滤掉。

      Object.prototype.hasOwnProperty()

    • 如何遍历不可枚举的对象。

      Reflect.ownKeys(obj)

    • 遍历以后遇到引用类型,需要递归处理。可能会引出第6点的循环引用问题。

  6. 循环引用,用 map、array 等数据结构存储已经处理过的对象,如果碰撞直接返回。这里推荐 weakMap。

相关链接

ramda - 0.27.1

JavaScript深拷贝看这篇就行了!(实现完美的ES6+版本) - CSDN