手写JavaScript 深拷贝

引言

在 JavaScript 开发中,数据拷贝是一个常见但容易出错的操作。浅拷贝只复制对象的引用,而深拷贝则创建对象的完全独立副本。本文将带你从零实现一个功能完整的深拷贝函数,并深入探讨其中的技术细节。

浅拷贝 vs 深拷贝

浅拷贝的局限性

javascript 复制代码
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };

shallowCopy.b.c = 3;
console.log(original.b.c); // 3 - 原对象也被修改了!

深拷贝的必要性

深拷贝创建完全独立的对象,修改副本不会影响原对象。

基础深拷贝实现

让我们从一个简单的深拷贝函数开始:

javascript 复制代码
function simpleDeepClone(obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    
    if (obj instanceof Array) {
        return obj.map(item => simpleDeepClone(item));
    }
    
    const cloned = {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            cloned[key] = simpleDeepClone(obj[key]);
        }
    }
    return cloned;
}

这个基础版本虽然能处理简单对象和数组,但存在严重缺陷。

完整深拷贝的挑战

1. 循环引用问题

javascript 复制代码
const obj = { a: 1 };
obj.self = obj; // 循环引用
// simpleDeepClone(obj) // 栈溢出!

2. 特殊对象类型

  • Map、Set、Date、RegExp 等内置对象
  • 函数对象
  • Symbol 属性

3. 不可枚举属性和 Symbol 键

完备深拷贝实现

下面是我们的完整解决方案:

javascript 复制代码
function deepClone(obj, map = new WeakMap()) {
    // 处理基本类型和函数
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    
    // 处理特殊对象类型
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Function) return obj;

    // 检查循环引用
    const objFromMap = map.get(obj);
    if (objFromMap) return objFromMap;

    // 保持构造函数原型
    const target = new obj.constructor();
    map.set(obj, target);

    // 处理 Map
    if (obj instanceof Map) {
        obj.forEach((value, key) => {
            target.set(deepClone(key, map), deepClone(value, map));
        });
        return target;
    }

    // 处理 Set
    if (obj instanceof Set) {
        obj.forEach(value => {
            target.add(deepClone(value, map));
        });
        return target;
    }

    // 使用 Reflect.ownKeys 获取所有属性(包括 Symbol 和不可枚举属性)
    const keys = Reflect.ownKeys(obj);
    for (let key of keys) {
        target[key] = deepClone(obj[key], map);
    }

    return target;
}

关键技术解析

1. 循环引用检测

使用 WeakMap 来记录已经拷贝过的对象:

javascript 复制代码
const map = new WeakMap();
map.set(obj, target); // 存储已拷贝对象

2. 保持构造函数链

javascript 复制代码
const target = new obj.constructor();

这确保了拷贝对象保持原对象的原型链,对于自定义类实例尤其重要。

3. Reflect.ownKeys 的强大能力

Reflect.ownKeys() 是我们实现的关键,它能够:

  • 获取字符串键(包括不可枚举的)
  • 获取 Symbol 键
  • 不遍历原型链

对比其他方法:

javascript 复制代码
const obj = { a: 1 };
const symbolKey = Symbol('private');
Object.defineProperty(obj, 'hidden', { 
    value: 2, 
    enumerable: false 
});
obj[symbolKey] = 3;

console.log(Object.keys(obj));           // ['a']
console.log(Object.getOwnPropertyNames(obj)); // ['a', 'hidden']
console.log(Reflect.ownKeys(obj));       // ['a', 'hidden', Symbol(private)]

4. 特殊对象的处理

每种特殊对象都需要特定的克隆策略:

javascript 复制代码
// Date 对象
if (obj instanceof Date) return new Date(obj);

// RegExp 对象  
if (obj instanceof RegExp) return new RegExp(obj);

// Map 对象
if (obj instanceof Map) {
    // 递归克隆键和值
}

// Set 对象
if (obj instanceof Set) {
    // 递归克隆值
}

测试用例

验证我们的深拷贝函数:

javascript 复制代码
// 测试循环引用
const circularObj = { a: 1 };
circularObj.self = circularObj;

// 测试复杂对象
const testObj = {
    number: 1,
    string: 'hello',
    array: [1, 2, { nested: 'object' }],
    date: new Date(),
    regex: /test/gi,
    map: new Map([['key', 'value']]),
    set: new Set([1, 2, 3]),
    symbol: Symbol('test'),
    function: function(x) { return x * 2; },
    [Symbol('private')]: 'private value'
};

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

const cloned = deepClone(testObj);

// 验证独立性
cloned.array[2].nested = 'modified';
console.log(testObj.array[2].nested); // 'object' - 原对象未被修改

// 验证循环引用
console.log(cloned.self === cloned); // true - 循环引用被正确处理

性能考虑

深拷贝是昂贵的操作,在实际使用中应该:

  1. 避免过度使用:只在必要时进行深拷贝
  2. 考虑替代方案:如不可变数据结构
  3. 使用 WeakMap:避免内存泄漏,WeakMap 的键是弱引用

边界情况处理

我们的实现还应该考虑:

javascript 复制代码
// 处理 Error 对象
if (obj instanceof Error) {
    const errorCopy = new obj.constructor(obj.message);
    errorCopy.stack = obj.stack;
    return errorCopy;
}

// 处理 Promise(通常不建议拷贝 Promise)
if (obj instanceof Promise) {
    return obj.then(deepClone);
}

// 处理 DOM 元素(通常直接返回)
if (obj instanceof Element) {
    return obj; // DOM 元素通常不应该被深拷贝
}

总结

实现一个完备的深拷贝函数需要考虑众多边界情况:

  1. 基本类型直接返回
  2. 循环引用通过 WeakMap 检测
  3. 特殊对象需要特殊处理
  4. 所有属性通过 Reflect.ownKeys 获取
  5. 原型链通过 constructor 保持

这个实现虽然相对完整,但在生产环境中仍可能需要根据具体需求进行调整。理解深拷贝的原理比记住实现更重要,这有助于我们在面对各种数据拷贝场景时做出正确的决策。

相关推荐
yeyuningzi8 小时前
npm升级提示error engine not compatible with your version of node/npm: npm@11.6.2
前端·npm·node.js
1024小神8 小时前
next 项目中的 'use client' 是什么意思
前端
我是华为OD~HR~栗栗呀8 小时前
24届-Python面经(华为OD)
java·前端·c++·python·华为od·华为·面试
whysqwhw8 小时前
mac上AndroidStudio升级无写入权限问题
前端
wyzqhhhh8 小时前
npm相关知识
前端·npm·node.js
卢叁9 小时前
Flutter之全局路由事件监听器RouteListenerManager
前端
盗德9 小时前
为什么要用Monorepo管理前端项目?(详解)
前端·架构·代码规范
五号厂房9 小时前
ProTable 大数据渲染优化:实现高性能表格编辑
前端
右子9 小时前
理解响应式设计—理念、实践与常见误解
前端·后端·响应式设计