深拷贝

10 小时前
/ ,
2

深拷贝

深拷贝(Deep Clone)指:复制出一个全新的数据结构,使得修改新对象的任意层级,不会影响原对象。

  • 浅拷贝:只拷贝一层引用(例如 {...obj}arr.slice()),嵌套对象仍共享引用。
  • 深拷贝:递归地拷贝所有可达子对象(并处理循环引用)。

什么时候需要深拷贝

  • 需要在不影响原数据的情况下做“试算/回滚”(例如表单草稿、撤销重做)
  • Redux/不可变数据没用到结构共享,需要简单“隔离”数据
  • 对象结构复杂且会被多处修改(避免意外的引用共享)

如果你的数据天然是“不可变”的(只读 + 通过新对象更新),通常不需要深拷贝。

常见方案对比

  • JSON.parse(JSON.stringify(x)):简单,但丢信息多,且不支持循环引用。
  • structuredClone(x):现代内置方案,支持循环引用与很多内置类型,但不能克隆函数/DOM 节点等。
  • 递归拷贝:可控、可扩展;需要自己处理类型与循环引用。

1) 最简方案:JSON 深拷贝(不推荐用于通用场景)

优点:写法短。

局限(面试高频):

  • functionsymbol 会丢失
  • undefined 会在对象属性里丢失(数组中会变成 null
  • Date/RegExp/Map/Set/Error 等类型会被“拍扁”成普通对象/字符串
  • 遇到循环引用直接报错
const source = {
  b: { name: 'xht' },
  foo: () => 5,
  3: undefined
};

const deep = JSON.parse(JSON.stringify(source));
// deep => { b: { name: 'xht' } }

2) 现代内置:structuredClone(优先推荐)

浏览器与 Node.js 新版本提供 structuredClone,特点:

  • 支持循环引用
  • 支持多种内置类型(如 DateMapSetArrayBuffer 等)
  • 不支持函数(会抛错)
// 运行环境支持时直接用
const copy = structuredClone({ a: 1, b: new Date() });

3) 递归版(无循环引用时可用)

优点:好理解。 缺点:不处理循环引用;对特殊类型支持弱。

export function baseClone<T>(input: T): T {
  if (typeof input !== 'object' || input === null) return input;

  const result: any = Array.isArray(input) ? [] : {};
  for (const key in input as any) {
    result[key] = baseClone((input as any)[key]);
  }
  return result;
}

4) 通用版:WeakMap 处理循环引用(推荐自实现方式)

为什么用 WeakMap

Map 也能实现“缓存已克隆对象”,但 Map 会强引用 key,可能造成缓存长期存在时的内存压力。

WeakMap 的 key 是弱引用:

  • 不阻止垃圾回收
  • 更不容易引入内存泄漏

一个更稳的 cloneDeep

特性:

  • 支持循环引用
  • 支持 DateRegExpMapSet
  • 保留原型(Object.create(proto)
  • 复制自有属性(包含 symbol key),尽量保留属性描述符(getter/setter 不会被“执行”)
export function cloneDeep<T>(input: T, cache = new WeakMap<object, any>()): T {
  // 基础类型、函数:直接返回
  if (typeof input !== 'object' || input === null) return input;
  if (typeof input === 'function') return input;

  const target = input as unknown as object;
  if (cache.has(target)) return cache.get(target);

  // 内置类型
  if (input instanceof Date) {
    const result = new Date(input.getTime());
    cache.set(target, result);
    return result as unknown as T;
  }

  if (input instanceof RegExp) {
    const result = new RegExp(input.source, input.flags);
    cache.set(target, result);
    return result as unknown as T;
  }

  if (input instanceof Map) {
    const result = new Map();
    cache.set(target, result);
    input.forEach((value, key) => {
      result.set(cloneDeep(key as any, cache), cloneDeep(value as any, cache));
    });
    return result as unknown as T;
  }

  if (input instanceof Set) {
    const result = new Set();
    cache.set(target, result);
    input.forEach((value) => {
      result.add(cloneDeep(value as any, cache));
    });
    return result as unknown as T;
  }

  // Array
  if (Array.isArray(input)) {
    const result: any[] = new Array(input.length);
    cache.set(target, result);
    for (let i = 0; i < input.length; i++) {
      result[i] = cloneDeep((input as any)[i], cache);
    }
    return result as unknown as T;
  }

  // 普通对象 / 类实例:保留原型 + 复制自有属性
  const proto = Object.getPrototypeOf(input);
  const result = Object.create(proto);
  cache.set(target, result);
  // Reflect.ownKeys取出自有key,不包括原型
  /**
   * 目的:拿到 所有自有属性键,包括:
    字符串 key(含不可枚举的)、symbol key
    为什么不用 for...in / Object.keys:
    for...in 会枚举 原型链 上的属性(不想要)
    Object.keys 只拿 可枚举的字符串 key(会漏掉不可枚举、会漏掉 symbol
   */
  Reflect.ownKeys(input as any).forEach((key) => {
    /**
     * 目的:拿到属性的 描述符 desc,里面包含:
        数据属性:{ value, writable, enumerable, configurable }
        访问器属性:{ get, set, enumerable, configurable }
        这么做的意义:
        你可以原样复制 enumerable/configurable/writable
        遇到 getter/setter 时,不会通过 input[key] 去“读取值”从而触发 getter(避免副作用)
     */
    const desc = Object.getOwnPropertyDescriptor(input as any, key);//  取出这个key的属性描述符
    if (!desc) return;

    if ('value' in desc) {
      desc.value = cloneDeep((input as any)[key as any], cache); 
    }
    Object.defineProperty(result, key, desc); // 替换 obj中的key新的描述符
  });

  return result;
}

示例:循环引用

const source2: any = {
  3: undefined,
  foo: () => 8,
  data: new Date(),
  tags: new Set(['a', 'b']),
  dict: new Map([['k', { v: 1 }]])
};
source2.self = source2;

const copied = cloneDeep(source2);
copied === source2; // false
copied.self === copied; // true(循环结构被保留)

复杂度

设对象图中可达节点总数为 nn(属性/元素/Map-Set entries 总量):

  • 时间:O(n)O(n)
  • 空间:O(n)O(n)(包含结果与 WeakMap 缓存)

常见坑点

  • 函数怎么处理:多数深拷贝实现会“按引用返回函数”,因为函数闭包环境无法被复制。
  • DOM 节点:不建议深拷贝 DOM;应该重新查询/重新创建。
  • 类实例:如果需要保留原型/方法,至少要 Object.create(proto),否则会退化为普通对象。
  • 属性描述符 / getter-setter:简单的 result[key] = ... 会触发 getter;如需严谨可复制 descriptor。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...