JavaScript 深度拷贝实现

深度拷贝(Deep Copy)是 JavaScript 中常见的需求,以下是多种实现方法和完整解决方案:

1. JSON 方法(最简单但有局限性)

function deepCloneByJSON(obj) {
  return JSON.parse(JSON.stringify(obj));
}

// 优点:简单快捷
// 缺点:
// 1. 无法拷贝函数、undefined、Symbol
// 2. 无法拷贝循环引用对象
// 3. 无法拷贝特殊对象(Date, RegExp, Map, Set等)
// 4. 会忽略原型链

// 示例
const obj = { a: 1, b: { c: 2 }, d: new Date(), e: function() {} };
const cloned = deepCloneByJSON(obj);
console.log(cloned.d); // 字符串,不是Date对象
console.log(cloned.e); // undefined

2. 基础递归实现

function deepClone(obj) {
  // 处理原始类型和函数
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 处理数组
  if (Array.isArray(obj)) {
    const arrCopy = [];
    for (let i = 0; i < obj.length; i++) {
      arrCopy[i] = deepClone(obj[i]);
    }
    return arrCopy;
  }
  
  // 处理对象
  const objCopy = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      objCopy[key] = deepClone(obj[key]);
    }
  }
  return objCopy;
}

3. 完整版深度拷贝(处理各种类型)

function deepCloneComplete(obj, hash = new WeakMap()) {
  // 处理基本类型和函数
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }
  
  // 处理Date
  if (obj instanceof Date) {
    const copy = new Date(obj.getTime());
    hash.set(obj, copy);
    return copy;
  }
  
  // 处理正则表达式
  if (obj instanceof RegExp) {
    const copy = new RegExp(obj.source, obj.flags);
    hash.set(obj, copy);
    return copy;
  }
  
  // 处理Map
  if (obj instanceof Map) {
    const copy = new Map();
    hash.set(obj, copy);
    obj.forEach((value, key) => {
      copy.set(deepCloneComplete(key, hash), deepCloneComplete(value, hash));
    });
    return copy;
  }
  
  // 处理Set
  if (obj instanceof Set) {
    const copy = new Set();
    hash.set(obj, copy);
    obj.forEach(value => {
      copy.add(deepCloneComplete(value, hash));
    });
    return copy;
  }
  
  // 处理数组
  if (Array.isArray(obj)) {
    const copy = [];
    hash.set(obj, copy);
    for (let i = 0; i < obj.length; i++) {
      copy[i] = deepCloneComplete(obj[i], hash);
    }
    return copy;
  }
  
  // 处理普通对象
  if (obj instanceof Object) {
    // 获取原对象的原型
    const copy = Object.create(Object.getPrototypeOf(obj));
    hash.set(obj, copy);
    
    // 拷贝所有属性(包括Symbol)
    const allKeys = [
      ...Object.getOwnPropertyNames(obj),
      ...Object.getOwnPropertySymbols(obj)
    ];
    
    for (const key of allKeys) {
      const descriptor = Object.getOwnPropertyDescriptor(obj, key);
      
      if (descriptor) {
        if (descriptor.get || descriptor.set) {
          // 处理访问器属性
          Object.defineProperty(copy, key, descriptor);
        } else {
          // 处理数据属性
          copy[key] = deepCloneComplete(obj[key], hash);
        }
      }
    }
    
    return copy;
  }
  
  // 其他类型直接返回
  return obj;
}

4. 性能优化版本

function deepCloneOptimized(obj, hash = new WeakMap()) {
  // 基础类型直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 循环引用检查
  if (hash.has(obj)) {
    return hash.get(obj);
  }
  
  // 根据构造函数类型创建对应实例
  const Constructor = obj.constructor;
  let copy;
  
  switch (Constructor) {
    case Date:
      copy = new Date(obj.getTime());
      break;
    case RegExp:
      copy = new Constructor(obj.source, obj.flags);
      break;
    case Map:
      copy = new Map();
      hash.set(obj, copy);
      obj.forEach((value, key) => {
        copy.set(
          deepCloneOptimized(key, hash),
          deepCloneOptimized(value, hash)
        );
      });
      return copy;
    case Set:
      copy = new Set();
      hash.set(obj, copy);
      obj.forEach(value => {
        copy.add(deepCloneOptimized(value, hash));
      });
      return copy;
    case Array:
      copy = [];
      hash.set(obj, copy);
      for (let i = 0; i < obj.length; i++) {
        copy[i] = deepCloneOptimized(obj[i], hash);
      }
      return copy;
    default:
      // 普通对象或自定义类实例
      copy = Object.create(Object.getPrototypeOf(obj));
      hash.set(obj, copy);
  }
  
  // 快速拷贝(使用Reflect.ownKeys获取所有键)
  const keys = Reflect.ownKeys(obj);
  for (const key of keys) {
    const descriptor = Object.getOwnPropertyDescriptor(obj, key);
    
    if (descriptor && (descriptor.get || descriptor.set)) {
      Object.defineProperty(copy, key, descriptor);
    } else {
      copy[key] = deepCloneOptimized(obj[key], hash);
    }
  }
  
  return copy;
}

5. 使用 MessageChannel 实现深度拷贝

function deepCloneMessageChannel(obj) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    
    port2.onmessage = (event) => resolve(event.data);
    port1.postMessage(obj);
  });
}

// 使用示例
async function example() {
  const obj = { a: 1, b: { c: 2 } };
  const cloned = await deepCloneMessageChannel(obj);
  console.log(cloned);
}

// 注意:这是异步的,且同样有一些限制
// 可以拷贝函数、循环引用,但不能拷贝DOM元素、函数上下文等

6. 结构化克隆 API(现代浏览器)

// 使用 structuredClone API(现代浏览器支持)
function deepCloneStructured(obj) {
  // 检查浏览器是否支持
  if (typeof structuredClone === 'function') {
    return structuredClone(obj);
  } else {
    // 降级方案
    console.warn('structuredClone not supported, using fallback');
    return deepCloneOptimized(obj);
  }
}

// 优点:
// 1. 浏览器原生支持,性能好
// 2. 支持更多类型(包括循环引用)
// 3. 标准API

7. 针对特定场景的优化版本

function deepCloneWithOptions(obj, options = {}) {
  const {
    hash = new WeakMap(),
    preserveProto = true,
    copyDescriptors = true,
    ignoreSymbols = false,
    maxDepth = 50
  } = options;
  
  // 深度限制
  if (maxDepth <= 0) {
    console.warn('Maximum depth reached');
    return obj;
  }
  
  // 基础类型检查
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 循环引用检查
  if (hash.has(obj)) {
    return hash.get(obj);
  }
  
  // 特殊类型处理
  const Constructor = obj.constructor;
  let copy;
  
  const newOptions = { ...options, maxDepth: maxDepth - 1 };
  
  switch (Constructor) {
    case Date:
      copy = new Date(obj.getTime());
      break;
    case RegExp:
      copy = new RegExp(obj.source, obj.flags);
      break;
    case Map:
      copy = new Map();
      hash.set(obj, copy);
      obj.forEach((value, key) => {
        copy.set(
          deepCloneWithOptions(key, newOptions),
          deepCloneWithOptions(value, newOptions)
        );
      });
      return copy;
    case Set:
      copy = new Set();
      hash.set(obj, copy);
      obj.forEach(value => {
        copy.add(deepCloneWithOptions(value, newOptions));
      });
      return copy;
    case Array:
      copy = [];
      hash.set(obj, copy);
      for (let i = 0; i < obj.length; i++) {
        copy[i] = deepCloneWithOptions(obj[i], newOptions);
      }
      return copy;
    default:
      // 创建新对象,保留原型链
      copy = preserveProto 
        ? Object.create(Object.getPrototypeOf(obj))
        : {};
      hash.set(obj, copy);
  }
  
  // 获取所有属性键
  let keys;
  if (ignoreSymbols) {
    keys = Object.getOwnPropertyNames(obj);
  } else {
    keys = [
      ...Object.getOwnPropertyNames(obj),
      ...Object.getOwnPropertySymbols(obj)
    ];
  }
  
  // 拷贝属性
  for (const key of keys) {
    if (copyDescriptors) {
      const descriptor = Object.getOwnPropertyDescriptor(obj, key);
      if (descriptor) {
        if (descriptor.get || descriptor.set) {
          Object.defineProperty(copy, key, descriptor);
        } else if ('value' in descriptor) {
          descriptor.value = deepCloneWithOptions(descriptor.value, newOptions);
          Object.defineProperty(copy, key, descriptor);
        }
      }
    } else {
      copy[key] = deepCloneWithOptions(obj[key], newOptions);
    }
  }
  
  return copy;
}

8. 使用示例

// 复杂对象示例
const complexObj = {
  number: 123,
  string: 'hello',
  boolean: true,
  null: null,
  undefined: undefined,
  array: [1, 2, { nested: 'value' }],
  date: new Date(),
  regexp: /test/gi,
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  symbol: Symbol('test'),
  function: function() { console.log('hello'); },
  // 循环引用
  circular: null
};

complexObj.circular = complexObj;
complexObj.nested = { parent: complexObj };

// 使用深度拷贝
const cloned = deepCloneOptimized(complexObj);

// 验证
console.log(cloned !== complexObj); // true
console.log(cloned.array !== complexObj.array); // true
console.log(cloned.date.getTime() === complexObj.date.getTime()); // true
console.log(cloned.circular === cloned); // true(保持了循环引用结构)

9. 性能测试对比

function benchmark() {
  const obj = createComplexObject(1000); // 创建复杂对象
  
  console.time('JSON方法');
  const jsonCopy = JSON.parse(JSON.stringify(obj));
  console.timeEnd('JSON方法');
  
  console.time('递归方法');
  const recursiveCopy = deepCloneOptimized(obj);
  console.timeEnd('递归方法');
  
  // 验证正确性
  console.log('JSON方法有效:', jsonCopy.array ? '是' : '否'); // 可能丢失数据
  console.log('递归方法有效:', recursiveCopy.array ? '是' : '否');
}

function createComplexObject(depth) {
  const obj = {
    array: new Array(100).fill(0).map((_, i) => i),
    nested: {},
    date: new Date(),
    map: new Map(),
    set: new Set()
  };
  
  if (depth > 0) {
    obj.nested = createComplexObject(depth - 1);
  }
  
  return obj;
}

选择建议

  1. 简单场景:使用 JSON.parse(JSON.stringify(obj))
  2. 现代浏览器:使用 structuredClone()
  3. 复杂场景:使用 deepCloneOptimizeddeepCloneComplete
  4. 需要异步:考虑 MessageChannel 方法
  5. 性能敏感:使用针对性的优化版本

注意事项

  1. 函数拷贝:函数通常不需要深度拷贝,因为它们很少被修改
  2. 原型链:根据需求决定是否保留原型链
  3. 不可枚举属性:考虑是否需要拷贝不可枚举属性
  4. 性能:深度拷贝大型对象可能消耗较多内存和时间
  5. 特殊对象:如 Buffer、ArrayBuffer 等可能需要特殊处理

根据具体需求选择合适的深度拷贝实现方案。


在JavaScript深度copy对象很多使用JSON.stringify 方法,当出现循环引用的情况下回报错,以下是判断循环引用的方法

1. 使用 WeakSet/Set 跟踪已访问对象

function hasCircularReference(obj) {
  const seen = new WeakSet();
  
  function detect(obj) {
    if (obj && typeof obj === 'object') {
      if (seen.has(obj)) {
        return true;
      }
      seen.add(obj);
      
      for (let key in obj) {
        if (obj.hasOwnProperty(key) && detect(obj[key])) {
          return true;
        }
      }
      seen.delete(obj); // 注意:对于树形遍历需要删除
    }
    return false;
  }
  
  return detect(obj);
}

// 示例
const obj = { a: 1 };
obj.self = obj; // 循环引用
console.log(hasCircularReference(obj)); // true

2. 优化的版本(更准确)

function hasCyclicReferences(obj) {
  const seen = new WeakSet();
  
  function detect(obj, path = []) {
    if (obj && typeof obj === 'object') {
      if (seen.has(obj)) {
        return true;
      }
      seen.add(obj);
      
      const keys = Object.keys(obj);
      for (let key of keys) {
        const value = obj[key];
        if (value && typeof value === 'object') {
          if (detect(value, [...path, key])) {
            return true;
          }
        }
      }
    }
    return false;
  }
  
  return detect(obj);
}

3. 使用 JSON.stringify 的 try-catch 方法

function hasCircularJSON(obj) {
  try {
    JSON.stringify(obj);
    return false;
  } catch (error) {
    // TypeError: Converting circular structure to JSON
    return error.message.includes('circular');
  }
}

// 注意:这种方法只能检测到最外层的循环引用

4. 完整功能检测函数(支持详细报告)

function findCircularReferences(obj) {
  const seen = new WeakMap(); // 存储对象和路径
  const circularPaths = [];
  
  function traverse(obj, path = []) {
    if (obj && typeof obj === 'object') {
      if (seen.has(obj)) {
        circularPaths.push({
          object: obj,
          path: path.join('.'),
          firstSeenAt: seen.get(obj)
        });
        return true;
      }
      
      seen.set(obj, path.join('.') || 'root');
      
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          const value = obj[key];
          if (value && typeof value === 'object') {
            traverse(value, [...path, key]);
          }
        }
      }
      
      seen.delete(obj);
    }
    return false;
  }
  
  traverse(obj);
  return {
    hasCircular: circularPaths.length > 0,
    circularPaths: circularPaths
  };
}

// 使用示例
const obj = {
  a: 1,
  b: { c: 2 }
};
obj.b.d = obj.b; // 循环引用

const result = findCircularReferences(obj);
console.log(result.hasCircular); // true
console.log(result.circularPaths);

5. 处理特殊情况的增强版

function detectCircular(obj, options = {}) {
  const {
    maxDepth = 50,
    ignoreBuiltIn = true,
    checkArrays = true,
    checkMapsAndSets = true
  } = options;
  
  const seen = new WeakSet();
  let depth = 0;
  
  function check(obj) {
    if (depth > maxDepth) {
      throw new Error('Maximum depth exceeded');
    }
    
    if (!obj || typeof obj !== 'object') {
      return false;
    }
    
    // 忽略内置对象(可选)
    if (ignoreBuiltIn) {
      const constructorName = obj.constructor.name;
      if (['Date', 'RegExp', 'Promise', 'Function'].includes(constructorName)) {
        return false;
      }
    }
    
    if (seen.has(obj)) {
      return true;
    }
    
    seen.add(obj);
    depth++;
    
    try {
      // 检查普通对象
      if (Array.isArray(obj) && checkArrays) {
        for (let item of obj) {
          if (check(item)) return true;
        }
      } 
      // 检查Map
      else if (obj instanceof Map && checkMapsAndSets) {
        for (let [key, value] of obj) {
          if (check(key) || check(value)) return true;
        }
      } 
      // 检查Set
      else if (obj instanceof Set && checkMapsAndSets) {
        for (let item of obj) {
          if (check(item)) return true;
        }
      } 
      // 检查普通对象
      else if (typeof obj === 'object') {
        for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
            if (check(obj[key])) return true;
          }
        }
      }
    } finally {
      depth--;
      seen.delete(obj);
    }
    
    return false;
  }
  
  return check(obj);
}

6. 性能优化的版本(避免递归栈溢出)

function hasCircularBFS(obj) {
  if (!obj || typeof obj !== 'object') return false;
  
  const seen = new WeakSet();
  const queue = [{ obj, path: [] }];
  
  while (queue.length > 0) {
    const { obj: current, path } = queue.shift();
    
    if (seen.has(current)) {
      return true;
    }
    
    seen.add(current);
    
    if (Array.isArray(current)) {
      for (let i = 0; i < current.length; i++) {
        const item = current[i];
        if (item && typeof item === 'object') {
          queue.push({ obj: item, path: [...path, i] });
        }
      }
    } else {
      for (let key in current) {
        if (current.hasOwnProperty(key)) {
          const value = current[key];
          if (value && typeof value === 'object') {
            queue.push({ obj: value, path: [...path, key] });
          }
        }
      }
    }
  }
  
  return false;
}

使用示例

// 创建循环引用
const circularObj = { name: 'John' };
circularObj.self = circularObj;

const nestedCircular = {
  a: { b: {} }
};
nestedCircular.a.b.c = nestedCircular.a;

// 检测
console.log(hasCircularReference(circularObj)); // true
console.log(hasCircularReference(nestedCircular)); // true

// 正常对象
const normalObj = { a: 1, b: { c: 2 } };
console.log(hasCircularReference(normalObj)); // false

注意事项

  1. WeakSet vs Set: 使用 WeakSet 不会阻止垃圾回收,更适合这种场景
  2. 性能考虑: 深度嵌套或大型对象可能需要性能优化
  3. 边缘情况: 处理数组、Map、Set等特殊对象
  4. 最大深度: 避免无限递归,可以设置最大深度限制
  5. 内存使用: 跟踪所有访问过的对象可能会消耗内存

推荐使用第一种或第二种方法,它们相对简单且有效。如果需要更详细的信息(如循环路径),可以使用第四种方法。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐