手撸实现深拷贝,自己的函数库里怎么能缺了这

73 阅读6分钟

前言:与其经常用其他库,不如手写实现下深拷贝,放入自己的函数库里随时备用来的直接。本文将手写两份深拷贝工具函数,给深拷贝一次性玩明白

一.首先要知道深拷贝的定义:

在堆中重新分配内存,并且把源对象所有属性都进行拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来对象是完全隔离,互不影响。

二.话不多说,直接上代码

1.适用于大部分场景的方法

// 一层一层手写深拷贝
/**
 * 深拷贝函数,用于递归复制对象或数组及其所有嵌套的对象和数组
 * @param {*} target - 需要进行深拷贝的目标对象或值
 * @returns {*} - 返回深拷贝后的新对象或原始值
 */
function deepClone(target){
    var cloneTarget;//最终克隆的新对象
    if(typeof target==='object'){
         if(Array.isArray(target)){
            //是数组
            cloneTarget=[];
            for(var i in target){
                cloneTarget.push(deepClone(target[i]));
            }
            return cloneTarget;
         }else if(target===null){
            //null的情况
            cloneTarget=null;
         }else if(target.constructor===Date){
            //Date情况
            cloneTarget=new Date(target.getTime());
         }else if(target.constructor===RegExp){
            //RegExp情况
            cloneTarget=new RegExp(target.source,target.flags);
         }else{
            //进入此分支,说明是一个对象
            cloneTarget={};
            for(var key in target){
               cloneTarget[key]=deepClone(target[key]);
            }
         }
    }else{
        //进入这,说明是诸如number、string、boolean之类的类型,直接赋值
        cloneTarget=target;
    }
    return cloneTarget;
}

//测试下
var obj={
    str:'string',
    bol:true,
    arr:[1,2,3],
    isobj:{
        a:'aaa',
        b:'bbb'
    },
    isfn:function(){
        console.log('这是function方法')
    }
}
var newObj=deepClone(obj);
obj.str='2222222';
obj.isobj.a='33333'
console.log('obj',obj)
console.log('newObj',newObj)

结果: 运行后更改原对象内复杂类型的值,新对象不做改变,深拷贝成功,结果如下:

image.png

2.进阶型,增强兼容的写法(适配特别复杂的场景,一般情景方法1即可解决)

// 兼容更多场景使用
function deepClone(target) {
    const map = new WeakMap(); // 处理循环引用
    return _clone(target);

    function _clone(obj) {
        // 基础类型直接返回
        if (obj === null) return null;
        if (typeof obj !== 'object' && typeof obj !== 'function') return obj;

        // 处理函数类型
        if (typeof obj === 'function') {
            return obj; // 函数一般不需要深拷贝
        }

        // 处理循环引用
        if (map.has(obj)) return map.get(obj);

        // 获取对象类型
        const type = Object.prototype.toString.call(obj);
        let clone;

        // 根据类型处理
        switch(type) {
            case '[object Array]':
                clone = [];
                break;
            case '[object Object]':
                clone = Object.create(Object.getPrototypeOf(obj));
                break;
            case '[object Date]':
                clone = new Date(obj.getTime());
                break;
            case '[object RegExp]':
                clone = new RegExp(obj.source, obj.flags);
                break;
            case '[object Map]':
                clone = new Map();
                break;
            case '[object Set]':
                clone = new Set();
                break;
            case '[object ArrayBuffer]':
                clone = obj.slice(0);
                break;
            default:
                try {
                    clone = new obj.constructor();
                } catch {
                    clone = Object.create(null); // 处理未知类型
                }
        }

        map.set(obj, clone);

        // 处理具体类型
        if (type === '[object Array]') {
            obj.forEach((item, i) => {
                clone[i] = _clone(item);
            });
        } else if (type === '[object Map]') {
            obj.forEach((value, key) => {
                //同时克隆键和值
                clone.set(_clone(key), _clone(value));
            });
        } else if (type === '[object Set]') {
            obj.forEach(value => {
                // 使用 add 方法
                clone.add(_clone(value));
            });
        } else if (type === '[object Object]') {
            // 处理所有属性(包括Symbol和不可枚举属性)
            const allKeys = [
                ...Object.getOwnPropertyNames(obj),
                ...Object.getOwnPropertySymbols(obj)
            ];
            allKeys.forEach(key => {
                const descriptor = Object.getOwnPropertyDescriptor(obj, key);
                if (descriptor.hasOwnProperty('value')) {
                    // 确保属性描述符正确处理
                    Object.defineProperty(clone, key, {
                        ...descriptor,
                        value: _clone(descriptor.value)
                    });
                } else {
                    Object.defineProperty(clone, key, descriptor);
                }
            });
        } else {
            // 处理其他对象的可枚举属性
            Object.keys(obj).forEach(key => {
                clone[key] = _clone(obj[key]);
            });
        }

        return clone;
    }
}

针对方法二编写一个专用测试函数:

/**
 * 编写一个专用测试函数
 */
function runTests() {
    console.log('开始测试 deepClone 函数...\n');

    // 测试1: 基本对象
    console.log('测试1: 基本对象');
    const obj1 = {
        str: 'string',
        bol: true,
        arr: [1, 2, 3],
        isobj: {
            a: 'aaa',
            b: 'bbb'
        },
        isfn: function(){
            console.log('这是function方法');
        }
    };
    const newObj1 = deepClone(obj1);
    obj1.str = '2222222';
    obj1.isobj.a = '33333';
    console.log('原始对象:', obj1);
    console.log('克隆对象:', newObj1);
    console.log('字符串属性隔离:', newObj1.str === 'string');
    console.log('嵌套对象隔离:', newObj1.isobj.a === 'aaa');
    console.log('函数引用:', newObj1.isfn === obj1.isfn);
    console.log('');

    // 测试2: Date 对象
    console.log('测试2: Date 对象');
    const date = new Date();
    const clonedDate = deepClone(date);
    console.log('原日期:', date);
    console.log('克隆日期:', clonedDate);
    console.log('是不同实例:', date !== clonedDate);
    console.log('时间值相同:', date.getTime() === clonedDate.getTime());
    console.log('');

    // 测试3: RegExp 对象
    console.log('测试3: RegExp 对象');
    const regex = new RegExp('/abc/gi');
    const clonedRegex = deepClone(regex);
    console.log('原正则:', regex);
    console.log('克隆正则:', clonedRegex);
    console.log('是不同实例:', regex !== clonedRegex);
    console.log('源相同:', regex.source === clonedRegex.source);
    console.log('');

    // 测试4: Map 和 Set
    console.log('测试4: Map 和 Set');
    const map = new Map([['key1', 'value1'], ['key2', { nested: 'obj' }]]);
    const set = new Set([1, 2, 3, 'string']);
    const clonedMap = deepClone(map);
    const clonedSet = deepClone(set);
    console.log('Map克隆成功:', clonedMap.get('key1') === 'value1');
    console.log('Set克隆成功:', clonedSet.has(1) && clonedSet.has(2) && clonedSet.has(3));
    console.log('');

    // 测试5: 循环引用
    console.log('测试5: 循环引用');
    const a = { name: 'a' };
    const b = { name: 'b' };
    a.ref = b;
    b.ref = a;
    
    try {
        const clonedA = deepClone(a);
        console.log('循环引用处理成功:', clonedA.ref.ref === clonedA);
        console.log('是不同实例:', clonedA !== a);
    } catch (e) {
        console.log('循环引用处理失败:', e.message);
    }
    console.log('');

    // 测试6: 数组
    console.log('测试6: 数组');
    const arr = [1, [2, 3], { a: 4 }];
    const clonedArr = deepClone(arr);
    arr[1][0] = 999;
    arr[2].a = 888;
    console.log('原数组:', arr);
    console.log('克隆数组:', clonedArr);
    console.log('嵌套数组隔离:', clonedArr[1][0] === 2);
    console.log('嵌套对象隔离:', clonedArr[2].a === 4);
    console.log('');

    console.log('测试完成');
}

// 运行测试
runTests();

//返回的测试结果如下:

// 开始测试 deepClone 函数...

// 测试1: 基本对象
// 原始对象: {
//   str: '2222222',
//   bol: true,
//   arr: [ 1, 2, 3 ],
//   isobj: { a: '33333', b: 'bbb' },
//   isfn: [Function: isfn]
// }
// 克隆对象: {
//   str: 'string',
//   bol: true,
//   arr: [ 1, 2, 3 ],
//   isobj: { a: 'aaa', b: 'bbb' },
//   isfn: [Function: isfn]
// }
// 字符串属性隔离: true
// 嵌套对象隔离: true
// 函数引用: true

// 测试2: Date 对象
// 原日期: 2025-08-04T13:29:07.691Z
// 克隆日期: 2025-08-04T13:29:07.691Z
// 是不同实例: true
// 时间值相同: true

// 测试3: RegExp 对象
// 原正则: /\/abc\/gi/
// 克隆正则: /\/abc\/gi/
// 是不同实例: true
// 源相同: true

// 测试4: Map 和 Set
// Map克隆成功: true
// Set克隆成功: true

// 测试5: 循环引用
// 循环引用处理成功: true
// 是不同实例: true

// 测试6: 数组
// 原数组: [ 1, [ 999, 3 ], { a: 888 } ]
// 克隆数组: [ 1, [ 2, 3 ], { a: 4 } ]
// 嵌套数组隔离: true
// 嵌套对象隔离: true

// 测试完成

三.方法2做了这些优化

优化说明:

  1. 类型判断优化

    • 使用Object.prototype.toString.call准确识别数据类型
    • 增加对Map/Set/ArrayBuffer等ES6数据结构的支持
  2. 循环引用处理

    • 使用WeakMap存储已克隆对象引用,避免栈溢出
  3. 对象特性保留

    • 通过Object.create保留原型链
    • 复制不可枚举属性和Symbol键
    • 使用Object.defineProperty保持属性特性
  4. 特殊对象处理

    • Date/RegExp通过构造函数重建
    • ArrayBuffer使用slice方法复制
    • Map/Set通过迭代器重建元素
  5. 代码结构优化

    • 使用递归函数和闭包封装WeakMap
    • 增加异常处理兜底机制
  6. 性能优化

    • 数组使用forEach遍历代替for...in
    • 避免不必要的原型链查找

最后

以上两种实现方式,都可以用来解决平常遇到的深拷贝相关的一些问题,按照项目数据需求使用即可。