对象深拷贝

完整实现

function deepClone(obj, hash = new WeakMap()) {
  // 1. 处理基本类型和 null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 2. 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 3. 处理特殊对象类型
  // 3.1 Date 对象
  if (obj instanceof Date) {
    return new Date(obj);
  }

  // 3.2 RegExp 对象
  if (obj instanceof RegExp) {
    const flags = obj.flags;
    const pattern = obj.source;
    return new RegExp(pattern, flags);
  }

  // 3.3 Map 对象
  if (obj instanceof Map) {
    const mapCopy = new Map();
    hash.set(obj, mapCopy);
    obj.forEach((value, key) => {
      mapCopy.set(deepClone(key, hash), deepClone(value, hash));
    });
    return mapCopy;
  }

  // 3.4 Set 对象
  if (obj instanceof Set) {
    const setCopy = new Set();
    hash.set(obj, setCopy);
    obj.forEach(value => {
      setCopy.add(deepClone(value, hash));
    });
    return setCopy;
  }

  // 3.5 Array 对象
  if (obj instanceof Array) {
    const arrCopy = [];
    hash.set(obj, arrCopy);
    obj.forEach((item, index) => {
      arrCopy[index] = deepClone(item, hash);
    });
    return arrCopy;
  }

  // 3.6 普通对象
  if (obj instanceof Object) {
    // 获取对象的所有属性描述符(包括不可枚举属性和 Symbol 属性)
    const allDescriptors = Object.getOwnPropertyDescriptors(obj);
    const objCopy = Object.create(Object.getPrototypeOf(obj), allDescriptors);
    
    hash.set(obj, objCopy);
    
    // 遍历所有属性(包括 Symbol 属性)
    [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)].forEach(key => {
      objCopy[key] = deepClone(obj[key], hash);
    });
    
    return objCopy;
  }

  // 4. 如果都不匹配,返回原值
  return obj;
}

详细原理讲解

1. 基本类型处理

if (obj === null || typeof obj !== 'object') {
  return obj;
}
  • 原理:JavaScript 的基本类型(string、number、boolean、undefined、symbol)和 null 是直接存储在栈内存中的值,赋值是值拷贝,所以直接返回原值即可
  • 为什么检查 nulltypeof null === 'object' 是 JavaScript 的历史遗留 bug,需要特殊处理

2. 循环引用处理

if (hash.has(obj)) {
  return hash.get(obj);
}
// ... 在创建新对象后
hash.set(obj, objCopy);
  • 原理:使用 WeakMap 存储已经克隆过的对象,key 是原对象,value 是克隆对象
  • 为什么用 WeakMap:WeakMap 的 key 是弱引用,不会阻止垃圾回收,避免内存泄漏
  • 解决什么问题:防止对象循环引用时导致无限递归,如 a.self = a

3. 特殊对象类型处理

3.1 Date 对象

if (obj instanceof Date) {
  return new Date(obj);
}
  • 原理:Date 对象存储的是时间戳,通过 new Date(originalDate) 可以创建具有相同时间的新 Date 对象
  • 为什么不能直接赋值:直接赋值复制的是引用,修改副本会影响原对象

3.2 RegExp 对象

if (obj instanceof RegExp) {
  const flags = obj.flags;
  const pattern = obj.source;
  return new RegExp(pattern, flags);
}
  • 原理:正则表达式由 pattern(模式)和 flags(标志)组成
  • flags 属性:ES6 新增,返回正则表达式的标志字符串(如 'g', 'i', 'm' 等)
  • source 属性:返回正则表达式的模式文本

3.3 Map 对象

if (obj instanceof Map) {
  const mapCopy = new Map();
  hash.set(obj, mapCopy);
  obj.forEach((value, key) => {
    mapCopy.set(deepClone(key, hash), deepClone(value, hash));
  });
  return mapCopy;
}
  • 原理:Map 的 key 和 value 都可能是引用类型,都需要深拷贝
  • 关键点:必须先创建空 Map 并存入 hash,再遍历填充,处理循环引用

3.4 Set 对象

if (obj instanceof Set) {
  const setCopy = new Set();
  hash.set(obj, setCopy);
  obj.forEach(value => {
    setCopy.add(deepClone(value, hash));
  });
  return setCopy;
}
  • 原理:Set 的元素可能是引用类型,需要深拷贝
  • 注意:Set 的元素是唯一的,克隆时要保持这种唯一性

3.5 Array 对象

if (obj instanceof Array) {
  const arrCopy = [];
  hash.set(obj, arrCopy);
  obj.forEach((item, index) => {
    arrCopy[index] = deepClone(item, hash);
  });
  return arrCopy;
}
  • 原理:数组是特殊的对象,需要保持索引顺序
  • 为什么用 forEach:保留空位(sparse array),map 会跳过空位

4. 普通对象的高级处理

if (obj instanceof Object) {
  // 获取所有属性描述符
  const allDescriptors = Object.getOwnPropertyDescriptors(obj);
  // 创建具有相同原型和属性描述符的新对象
  const objCopy = Object.create(Object.getPrototypeOf(obj), allDescriptors);
  
  hash.set(obj, objCopy);
  
  // 遍历所有属性进行深拷贝
  [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)].forEach(key => {
    objCopy[key] = deepClone(obj[key], hash);
  });
  
  return objCopy;
}

这部分的详细原理:

4.1 保留原型链

Object.getPrototypeOf(obj)
  • 获取对象的原型,确保克隆对象与原对象有相同的原型链

4.2 保留属性描述符

Object.getOwnPropertyDescriptors(obj)
  • 获取对象所有属性的完整描述符(value、writable、enumerable、configurable、get、set)
  • 确保克隆对象保留原属性的特性,包括 getter/setter

4.3 创建新对象

Object.create(proto, descriptors)
  • 第一个参数:指定新对象的原型
  • 第二个参数:属性描述符对象,直接设置到新对象上
  • 这样创建的对象完美继承了原对象的所有特性

4.4 处理 Symbol 属性

[...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)]
  • Object.keys() 获取可枚举的字符串属性
  • Object.getOwnPropertySymbols() 获取所有 Symbol 属性
  • 展开运算符组合两者,确保所有类型的属性都被克隆

使用示例

// 测试数据
const obj = {
  num: 1,
  str: 'hello',
  bool: true,
  date: new Date(),
  reg: /test/gi,
  arr: [1, 2, { nested: 'object' }],
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  [Symbol('sym')]: 'symbol property',
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  }
};

// 循环引用测试
obj.self = obj;

// 执行克隆
const cloned = deepClone(obj);

// 验证
console.log(cloned !== obj); // true
console.log(cloned.date !== obj.date); // true
console.log(cloned.reg !== obj.reg); // true
console.log(cloned.arr[2] !== obj.arr[2]); // true
console.log(cloned.fullName); // undefined(getter 被正确复制但需要上下文)

局限性说明

  1. 函数:一般保持引用不变,因为函数的作用域链无法复制
  2. DOM 节点:需要特殊处理,通常使用 cloneNode()
  3. 内置对象:如 Buffer、Promise 等需要特殊处理
  4. WeakMap/WeakSet:无法遍历,无法复制

这个实现覆盖了 95% 以上的日常使用场景,是一个工业级的深拷贝函数。


对象深拷贝
http://localhost:8090/archives/dui-xiang-shen-kao-bei
作者
Administrator
发布于
2026年02月27日
许可协议