【前端面试小册】JS-第18节:深拷贝进阶版

第18节:深拷贝进阶版

一、深拷贝的概念

1.1 什么是深拷贝

深拷贝是指创建一个新对象,新对象的所有属性都是原对象属性的完全独立副本,修改新对象不会影响原对象。

对比浅拷贝

  • 浅拷贝:只复制第一层,嵌套对象仍然共享引用
  • 深拷贝:递归复制所有层级,完全独立
// 浅拷贝
const obj = { a: 1, b: { c: 2 } };
const shallow = { ...obj };
shallow.b.c = 3;
console.log(obj.b.c);  // 3(被修改了)

// 深拷贝
const deep = deepCopy(obj);
deep.b.c = 3;
console.log(obj.b.c);  // 2(未被修改)

二、方法一:JSON.stringify()

2.1 基本实现

function deepCopy(target) {
    return JSON.parse(JSON.stringify(target));
}

2.2 使用示例

let obj = {
    a: 0,
    b: {
        c: 0,
    }
};

const objCopy = deepCopy(obj);

obj.a = 1;
obj.b.c = 1;

console.log(obj);      // { a: 1, b: { c: 1 } }
console.log(objCopy);  // { a: 0, b: { c: 0 } }
// 上面改变源对象 c 的值不会影响复制产生的对象

2.3 JSON.stringify 的问题

局限性

  1. 忽略 undefinedsymbol、无法拷贝函数
  2. 不能正确处理 RegExpDateSetMap
  3. 不能处理正则
  4. 循环引用会导致无限递归,栈溢出

2.4 问题演示

let obj_1 = {
    a: 0,
    b: undefined,
    c: Symbol(2),
    d: new RegExp(/a/g),
    e: new Set([1, 1]),
    f: new Map([['age', 2]])
};

let obj_1_Copy = deepCopy(obj_1);

console.log(obj_1_Copy);  // { a: 0, d: {}, e: {}, f: {} }

可以看到拷贝后的值有以下问题

  • undefinedb 直接被忽略
  • Symbolc 被忽略
  • 正则 d 变成了空对象 {}
  • Sete 变成了空对象 {}
  • Mapf 变成了空对象 {}

三、方法二:简单递归实现

3.1 基本实现

function deepCopy(target) {
    if (target instanceof Object) {
        // 处理函数
        if (target instanceof Function) {
            return function() {
                return target.call(this, ...arguments);
            };
        }
        // 处理正则
        if (target instanceof RegExp) {
            return new RegExp(target.source, target.flags);
        }
        // 处理日期
        if (target instanceof Date) {
            return new Date(target);
        }
        // 处理 Map
        if (target instanceof Map) {
            return new Map(target);
        }
        // 处理 Set
        if (target instanceof Set) {
            return new Set(target);
        }
        
        // 处理数组和对象
        const copy = Array.isArray(target) ? [] : {};
        for (const key in target) {
            if (target.hasOwnProperty(key)) {
                copy[key] = deepCopy(target[key]);
            }
        }
        return copy;
    }
    return target;
}

3.2 使用示例

const obj = {
    bigInt: BigInt(22),
    symbol: Symbol(2),
    set: new Set([1]),
    map: new Map([[1, 2]]),
};

const objCopy = deepCopy(obj);
objCopy.set.add(88);

console.log('obj', obj);
// {
//   bigInt: 22n,
//   symbol: Symbol(2),
//   set: Set(1) { 1 },
//   map: Map(1) { 1 => 2 }
// }

console.log('objCopy', objCopy);
// {
//   bigInt: 22n,
//   symbol: Symbol(2),
//   set: Set(2) { 1, 88 },
//   map: Map(1) { 1 => 2 }
// }

// 发现复制产生的对象不受影响

3.3 问题:循环引用

const obj = {
    a: 1,
    b: undefined,
    c: {
        name: '愚公上岸说'
    },
    d: [2]
};
obj.obj = obj;  // 循环引用

const copy = deepCopy(obj);
// 循环引用导致栈溢出:Maximum call stack size exceeded

问题分析

graph TD
    A[开始拷贝 obj] --> B[拷贝属性 a]
    B --> C[拷贝属性 c]
    C --> D[拷贝 c.name]
    D --> E[拷贝属性 obj]
    E --> F[发现 obj 指向原对象]
    F --> G[再次拷贝 obj]
    G --> H[无限递归]
    H --> I[栈溢出]

四、方法三:终极版(防止循环引用)

4.1 核心思路

解决循环引用问题,额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系。

当需要拷贝当前对象时,先去存储空间中找,如果拷贝过的话直接返回;如果没有的话继续拷贝,这样就解决了循环引用的问题。

4.2 完整实现

// 克隆函数
function cloneFun(fn) {
    const fnStr = fn.toString();
    if (fn.prototype) {
        // 有 prototype 的函数(构造函数)
        return function() {
            return fn.call(this, ...arguments);
        };
    } else {
        // 箭头函数
        return eval(fnStr);
    }
}

// 防止循环引用版本
function deepClone(target, cache = new WeakMap()) {
    // 单独开辟 cache,存储当前对象和拷贝对象的对应关系
    if (cache.has(target)) {
        return cache.get(target);
    }
    
    let copy;
    
    if (typeof target === 'object' && target !== null) {
        const type = Object.prototype.toString.call(target).match(/\s+(\w+)/)[1];
        
        switch (type) {
            case 'Function':
                copy = cloneFun(target);
                break;
            case 'RegExp':
                copy = new RegExp(target.source, target.flags);
                break;
            case 'Set':
                copy = new Set();
                break;
            case 'Map':
                copy = new Map();
                break;
            case 'Date':
                copy = new Date(target);
                break;
            default:
                copy = Array.isArray(target) ? [] : {};
        }
        
        // 将属性和拷贝后的值作为一个 map
        cache.set(target, copy);
        
        // 处理 Set
        if (type === 'Set') {
            target.forEach(value => {
                copy.add(deepClone(value, cache));
            });
            return copy;
        }
        
        // 处理 Map
        if (type === 'Map') {
            target.forEach((value, key) => {
                copy.set(key, deepClone(value, cache));
            });
            return copy;
        }
        
        // 处理普通对象和数组
        if (copy) {
            for (let key in target) {
                if (target.hasOwnProperty(key)) {
                    let val = target[key];
                    copy[key] = deepClone(val, cache);
                }
            }
            return copy;
        }
    }
    
    // 基本类型直接返回
    return target;
}

4.3 执行流程

graph TD
    A[开始拷贝 target] --> B{是否在 cache 中?}
    B -->|是| C[返回已拷贝的对象]
    B -->|否| D{判断类型}
    D -->|Function| E[克隆函数]
    D -->|RegExp| F[创建新正则]
    D -->|Date| G[创建新日期]
    D -->|Set| H[创建新 Set]
    D -->|Map| I[创建新 Map]
    D -->|Array| J[创建新数组]
    D -->|Object| K[创建新对象]
    D -->|基本类型| L[直接返回]
    E --> M[存入 cache]
    F --> M
    G --> M
    H --> N[递归拷贝 Set 元素]
    I --> O[递归拷贝 Map 键值]
    J --> P[递归拷贝数组元素]
    K --> Q[递归拷贝对象属性]
    N --> M
    O --> M
    P --> M
    Q --> M
    M --> R[返回拷贝结果]

4.4 关键点解析

4.4.1 WeakMap 的作用

cache = new WeakMap();

为什么使用 WeakMap

  • WeakMap 的键必须是对象,值可以是任意类型
  • WeakMap 的键是弱引用,不会阻止垃圾回收
  • 适合存储对象之间的映射关系

4.4.2 循环引用检测

if (cache.has(target)) {
    return cache.get(target);
}

原理

  • 在拷贝前检查对象是否已经在 cache
  • 如果存在,说明已经拷贝过,直接返回已拷贝的对象
  • 避免了无限递归

4.4.3 函数克隆

function cloneFun(fn) {
    const fnStr = fn.toString();
    if (fn.prototype) {
        // 普通函数
        return function() {
            return fn.call(this, ...arguments);
        };
    } else {
        // 箭头函数
        return eval(fnStr);
    }
}

区别

  • 普通函数:有 prototype,需要保持 this 绑定
  • 箭头函数:没有 prototype,可以直接 eval 字符串

4.5 测试用例

const obj = {
    a: 1,
    b: undefined,
    c: {
        name: '愚公上岸说',
    },
    d: [2],
    fn: () => { return 2; },
    map: new Map([[1, 2]])
};
obj.obj = obj;  // 循环引用

const copy = deepClone(obj);

obj.c.name = 2;

console.log('obj---', obj);
// obj--- <ref *1> {
//   a: 1,
//   b: undefined,
//   c: { name: 2 },
//   d: [ 2 ],
//   fn: [Function: fn],
//   map: Map(1) { 1 => 2 },
//   obj: [Circular *1]
// }

console.log('copy--', copy);
// copy-- <ref *1> {
//   a: 1,
//   b: undefined,
//   c: { name: '愚公上岸说' },
//   d: [ 2 ],
//   fn: [Function: fn],
//   map: Map(1) { 1 => 2 },
//   obj: [Circular *1]
// }

验证结果

  • ✅ 循环引用问题已解决
  • ✅ 修改原对象不影响拷贝对象
  • ✅ 所有类型都能正确拷贝

五、优化版本

5.1 支持更多类型

function deepClone(target, cache = new WeakMap()) {
    if (cache.has(target)) {
        return cache.get(target);
    }
    
    let copy;
    
    if (typeof target === 'object' && target !== null) {
        const type = Object.prototype.toString.call(target).match(/\s+(\w+)/)[1];
        
        switch (type) {
            case 'Function':
                copy = cloneFun(target);
                break;
            case 'RegExp':
                copy = new RegExp(target.source, target.flags);
                break;
            case 'Set':
                copy = new Set();
                break;
            case 'Map':
                copy = new Map();
                break;
            case 'Date':
                copy = new Date(target);
                break;
            case 'Error':
                copy = new Error(target.message);
                break;
            case 'ArrayBuffer':
                copy = target.slice();
                break;
            default:
                copy = Array.isArray(target) ? [] : {};
        }
        
        cache.set(target, copy);
        
        // 处理 Set
        if (type === 'Set') {
            target.forEach(value => {
                copy.add(deepClone(value, cache));
            });
            return copy;
        }
        
        // 处理 Map
        if (type === 'Map') {
            target.forEach((value, key) => {
                copy.set(deepClone(key, cache), deepClone(value, cache));
            });
            return copy;
        }
        
        // 处理普通对象和数组
        if (copy) {
            for (let key in target) {
                if (target.hasOwnProperty(key)) {
                    copy[key] = deepClone(target[key], cache);
                }
            }
            return copy;
        }
    }
    
    return target;
}

5.2 性能优化

function deepClone(target, cache = new WeakMap()) {
    // 基本类型直接返回
    if (target === null || typeof target !== 'object') {
        return target;
    }
    
    // 检查缓存
    if (cache.has(target)) {
        return cache.get(target);
    }
    
    // ... 其余代码
}

六、方法对比

方法 优点 缺点 适用场景
JSON.stringify 实现简单 不支持函数、Date、RegExp、循环引用 简单对象
简单递归 支持更多类型 不支持循环引用 无循环引用场景
终极版 功能完整 实现复杂 生产环境

七、面试要点总结

核心知识点

  1. 深拷贝 vs 浅拷贝:深拷贝递归复制所有层级
  2. 循环引用处理:使用 WeakMap 存储已拷贝对象
  3. 类型处理:需要特殊处理函数、Date、RegExp、Set、Map 等
  4. 性能优化:缓存已拷贝对象,避免重复拷贝

常见面试题

Q1: 如何实现深拷贝?

答:递归遍历对象,对每个属性进行拷贝。需要处理特殊类型(函数、Date、RegExp 等),并使用 WeakMap 解决循环引用问题。

Q2: 如何解决循环引用?

答:使用 WeakMap 存储已拷贝的对象,在拷贝前检查对象是否已存在,如果存在直接返回,避免无限递归。

Q3: JSON.stringify 实现深拷贝有什么问题?

答:

  • 不支持函数、undefinedSymbol
  • 不能正确处理 DateRegExpSetMap
  • 循环引用会导致栈溢出

实战建议

  • ✅ 生产环境可以使用 lodash.cloneDeep
  • ✅ 理解深拷贝的原理有助于解决实际问题
  • ✅ 注意循环引用和特殊类型的处理
  • ✅ 考虑性能优化,避免不必要的拷贝
#前端面试##面试#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

10-30 16:31
重庆大学 Java
代码飞升_不回私信人...:你说你善于学习,大家都会说。你说你是985,985会替你表达一切
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务