【前端面试知识点】JavaScript:深拷贝和浅拷贝的区别,如何实现一个深拷贝函数(考虑循环引用、特殊对象如Date、RegExp等)?

0 阅读3分钟

深拷贝与浅拷贝的区别

浅拷贝:只复制对象的第一层属性。如果属性值是基本类型,则拷贝其值;如果是引用类型(对象、数组等),则拷贝其引用。因此,浅拷贝后的对象与原对象共享引用类型的属性,修改一方会影响另一方。

深拷贝:递归地复制对象的所有层级,生成一个完全独立的新对象,与原对象没有任何引用关系。修改深拷贝后的对象不会影响原对象。

可以把浅拷贝和深拷贝想象成复印文件重新誊抄

  • 浅拷贝:就像复印一张表格,表格里的文字(基本类型)都复制下来了,但如果表格里夹着一张照片(引用类型),复印出来的文件里只是照片的复印件,而原照片只有一个。所以如果你在复印件上修改照片,原照片也会被改动,因为它们其实是同一张底片。
  • 深拷贝:就像把表格里的每个字重新写一遍,连那张照片也单独重新冲洗一张。这样最终得到两份完全独立的文件,改哪一份都不会影响另一份。

示例

javascript

const original = {
  a: 1,
  b: { c: 2 }
};
const shallow = { ...original }; // 浅拷贝
shallow.b.c = 3;
console.log(original.b.c); // 3 — 原对象也被修改

const deep = JSON.parse(JSON.stringify(original)); // 一种深拷贝方式
deep.b.c = 4;
console.log(original.b.c); // 3 — 原对象不受影响

实现一个深拷贝函数(支持循环引用、Date、RegExp等)

以下实现覆盖了绝大多数常见场景,包括:

  • 基本类型、函数(直接复用)
  • 特殊对象:Date、RegExp、Map、Set、ArrayBuffer、TypedArray
  • 循环引用(使用 WeakMap)
  • 保留原型链
  • 处理 Symbol 属性
  • 数组与普通对象

javascript

function deepClone(value, cache = new WeakMap()) {
  // 基本类型、null、undefined、函数直接返回
  if (value === null || typeof value !== 'object') {
    return value;
  }

  // 处理循环引用
  if (cache.has(value)) {
    return cache.get(value);
  }

  // 处理 Date
  if (value instanceof Date) {
    return new Date(value);
  }

  // 处理 RegExp
  if (value instanceof RegExp) {
    return new RegExp(value.source, value.flags);
  }

  // 处理 Map
  if (value instanceof Map) {
    const clonedMap = new Map();
    cache.set(value, clonedMap);
    for (const [key, val] of value) {
      clonedMap.set(deepClone(key, cache), deepClone(val, cache));
    }
    return clonedMap;
  }

  // 处理 Set
  if (value instanceof Set) {
    const clonedSet = new Set();
    cache.set(value, clonedSet);
    for (const val of value) {
      clonedSet.add(deepClone(val, cache));
    }
    return clonedSet;
  }

  // 处理 ArrayBuffer
  if (value instanceof ArrayBuffer) {
    return value.slice(0);
  }

  // 处理 TypedArray 和 DataView
  if (ArrayBuffer.isView(value)) {
    const buffer = value.buffer;
    const clonedBuffer = buffer.slice(0);
    // 根据不同类型创建新实例
    const Constructor = value.constructor;
    return new Constructor(clonedBuffer, value.byteOffset, value.length);
  }

  // 处理普通对象和数组:保留原型
  const proto = Object.getPrototypeOf(value);
  const cloned = Array.isArray(value) ? [] : Object.create(proto);
  cache.set(value, cloned);

  // 拷贝所有可枚举属性(包括 Symbol 属性)
  const keys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)];
  for (const key of keys) {
    cloned[key] = deepClone(value[key], cache);
  }

  return cloned;
}

关键点说明

  1. 基本类型与函数:直接返回,函数体不会改变,浅拷贝即可。
  2. 循环引用:使用 WeakMap 存储已拷贝的对象,在递归时检测到已存在则直接返回,避免无限递归。
  3. Date 和 RegExp:通过构造函数创建新实例,确保独立。
  4. Map 和 Set:递归克隆其键值对或元素。
  5. ArrayBuffer 与 TypedArray:复制底层缓冲区,再根据原类型构建新视图。
  6. 原型保留:通过 Object.create(proto) 创建对象,保留原对象的原型链(例如自定义类的实例)。
  7. Symbol 属性:使用 Object.getOwnPropertySymbols 获取并拷贝,保证属性完整性。
  8. 性能WeakMap 可避免内存泄漏,且不阻止垃圾回收。

使用示例

javascript

const obj = {
  num: 1,
  str: 'hello',
  date: new Date(),
  regex: /abc/gi,
  map: new Map([['a', 1]]),
  set: new Set([1, 2]),
  buffer: new ArrayBuffer(8),
  uint8: new Uint8Array([1,2,3]),
  nested: { x: 10 },
  self: null
};
obj.self = obj; // 循环引用

const cloned = deepClone(obj);
console.log(cloned !== obj);                     // true
console.log(cloned.date !== obj.date);           // true
console.log(cloned.regex !== obj.regex);         // true
console.log(cloned.map !== obj.map);             // true
console.log(cloned.set !== obj.set);             // true
console.log(cloned.buffer !== obj.buffer);       // true
console.log(cloned.uint8 !== obj.uint8);         // true
console.log(cloned.self === cloned);             // true (循环引用正常)
console.log(cloned.nested !== obj.nested);       // true

注意事项

  • 此实现不处理 DOM 元素、Error 对象等特殊宿主对象,若需支持可扩展对应分支。
  • 函数、Symbol 作为属性值时已处理,但函数本身不深拷贝,通常已足够。
  • 若需拷贝不可枚举属性或属性描述符,可使用 Object.getOwnPropertyDescriptors,但通常深拷贝仅关注数据。