Skip to content

深拷贝

题目描述

实现一个完整的深拷贝函数,要求:

  • 支持对象和数组的深层复制
  • 解决循环引用问题
  • 处理各种数据类型(Date、RegExp、Symbol 等)
  • 保持原对象的所有特性和属性

本题考查 递归算法数据类型判断循环引用处理 的综合能力。

核心知识点

1. 深拷贝 vs 浅拷贝

  • 浅拷贝: 只复制对象的第一层属性,深层属性仍共享引用
  • 深拷贝: 递归复制所有层级的属性,创建完全独立的副本
  • 应用场景: 防止意外修改、状态管理、数据备份

2. 循环引用问题

javascript
const a = { name: 'Alice' }
a.self = a // 创建循环引用

// 不处理循环引用会导致:
// RangeError: Maximum call stack size exceeded

3. JavaScript 数据类型分类

  • 基本类型: number, string, boolean, undefined, null, symbol, bigint
  • 引用类型: Object, Array, Date, RegExp, Function, Map, Set 等
  • 特殊处理: Symbol 键、不可枚举属性、getter/setter

代码实现

javascript
export default function deepClone(obj, cache = new Map()) {
  // 处理循环引用:如果已经克隆过,直接返回
  if (cache.has(obj)) {
    return cache.get(obj)
  }

  let result = null

  if (Array.isArray(obj)) {
    result = handleArray(obj, cache)
  }
  else if (isObject(obj)) {
    result = handleObject(obj, cache)
  }
  else {
    result = handleBasic(obj)
  }

  return result
}

// 处理数组
function handleArray(arr, cache) {
  const tmp = []

  // 先将结果存入缓存,防止循环引用
  cache.set(arr, tmp)

  arr.forEach((el, idx) => {
    tmp[idx] = deepClone(el, cache)
  })

  return tmp
}

// 处理对象
function handleObject(obj, cache) {
  // 处理特殊对象类型
  if (obj instanceof Date) {
    return new Date(obj.getTime())
  }

  if (obj instanceof RegExp) {
    return new RegExp(obj.source, obj.flags)
  }

  if (obj instanceof Error) {
    const error = new Error(obj.message)
    error.name = obj.name
    error.stack = obj.stack
    return error
  }

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

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

  // 处理普通对象
  const tmp = {}
  cache.set(obj, tmp) // 先存入缓存防止循环引用

  // 复制所有可枚举属性(包括 Symbol 键)
  const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)]

  keys.forEach((key) => {
    tmp[key] = deepClone(obj[key], cache)
  })

  return tmp
}

// 处理基本类型
function handleBasic(obj) {
  return obj // 基本类型直接返回
}

// 判断是否为对象
function isObject(obj) {
  return obj !== null && typeof obj === 'object'
}

关键技术点

1. 循环引用检测与处理

javascript
// 使用 Map 缓存已克隆的对象
const cache = new Map();

// 检测循环引用
if (cache.has(obj)) {
  return cache.get(obj); // 返回已克隆的对象
}

// 先存入缓存,再进行递归克隆
cache.set(obj, result);

2. 数据类型精确判断

javascript
// 数组判断
Array.isArray(obj)

// 对象类型判断
obj instanceof Date
obj instanceof RegExp
obj instanceof Error

// 基本类型判断
typeof obj === 'object' && obj !== null

3. Symbol 属性处理

javascript
// 获取包括 Symbol 在内的所有属性
const keys = [
  ...Object.keys(obj), // 字符串键
  ...Object.getOwnPropertySymbols(obj), // Symbol 键
]

4. 特殊对象的克隆技巧

javascript
// Date 对象
new Date(obj.getTime())

// RegExp 对象
new RegExp(obj.source, obj.flags)

// Error 对象
const error = new Error(obj.message)
error.name = obj.name
error.stack = obj.stack

5. 常见陷阱和坑点

  • 忘记处理循环引用: 导致栈溢出
  • 遗漏 Symbol 属性: Symbol 键被忽略
  • 特殊对象处理不当: Date、RegExp 等失去特性
  • 原型链问题: 只克隆自有属性,不包括原型
  • 不可枚举属性: Object.keys() 无法获取

使用示例

javascript
// 基本对象拷贝
const original = {
  name: 'John',
  age: 30,
  hobbies: ['reading', 'coding'],
  address: {
    city: 'New York',
    country: 'USA',
  },
}

const cloned = deepClone(original)
cloned.address.city = 'Boston'
console.log(original.address.city) // 'New York' (未受影响)

// 循环引用处理
const obj = { name: 'Alice' }
obj.self = obj
obj.friend = { name: 'Bob', bestFriend: obj }

const clonedObj = deepClone(obj)
console.log(clonedObj.self === clonedObj) // true
console.log(clonedObj.friend.bestFriend === clonedObj) // true

// 特殊类型处理
const complex = {
  date: new Date('2023-01-01'),
  regex: /hello/gi,
  error: new Error('test error'),
  map: new Map([
    ['key1', 'value1'],
    ['key2', { nested: true }],
  ]),
  set: new Set([1, 2, 3, { a: 1 }]),
  symbol: Symbol('test'),
  [Symbol('key')]: 'symbol value',
}

const clonedComplex = deepClone(complex)
console.log(clonedComplex.date instanceof Date) // true
console.log(clonedComplex.regex instanceof RegExp) // true
console.log(clonedComplex.map instanceof Map) // true

// 数组深拷贝
const nestedArray = [
  1,
  'string',
  [2, 3, [4, 5]],
  { a: 1, b: [6, 7] },
  new Date(),
]

const clonedArray = deepClone(nestedArray)
clonedArray[2][2][0] = 999
console.log(nestedArray[2][2][0]) // 4 (未受影响)

// 函数处理(通常不克隆,保持引用)
const withFunction = {
  data: [1, 2, 3],
  method() {
    return this.data.length
  },
}

const clonedWithFunction = deepClone(withFunction)
console.log(clonedWithFunction.method === withFunction.method) // true

记忆要点

核心记忆点

  1. 循环引用检测 - 使用 Map 缓存已克隆对象
  2. 类型精确判断 - instanceof 和 typeof 结合使用
  3. Symbol 属性 - 使用 getOwnPropertySymbols 获取
  4. 特殊对象处理 - Date、RegExp、Error 等需要特殊构造
  5. 递归克隆 - 深度优先遍历所有属性

处理顺序记忆

javascript
// 1. 检查缓存(循环引用)
// 2. 判断类型(数组、对象、基本类型)
// 3. 创建新容器并缓存
// 4. 递归克隆所有属性
// 5. 返回克隆结果

扩展思考

1. 性能优化版本

javascript
function deepCloneOptimized(obj, cache = new WeakMap()) {
  // 使用 WeakMap 优化内存使用
  if (cache.has(obj)) {
    return cache.get(obj)
  }

  // 快速路径:基本类型直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // 构造函数映射,避免重复 instanceof 检查
  const ctorMap = new Map([
    [Date, o => new Date(o.getTime())],
    [RegExp, o => new RegExp(o.source, o.flags)],
    [Array, o => []],
    [Object, (o) => {}],
  ])

  const ctor = obj.constructor
  const cloner = ctorMap.get(ctor)

  if (!cloner) {
    return obj // 不支持的类型直接返回
  }

  const result = cloner(obj)
  cache.set(obj, result)

  if (ctor === Array || ctor === Object) {
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        result[key] = deepCloneOptimized(obj[key], cache)
      }
    }
  }

  return result
}

2. 支持自定义克隆策略

javascript
function deepCloneWithStrategy(obj, options = {}) {
  const {
    includeNonEnumerable = false,
    includeSymbols = true,
    cloneFunction = false,
    maxDepth = Infinity,
    currentDepth = 0,
  } = options

  // 深度限制
  if (currentDepth >= maxDepth) {
    return obj
  }

  const cache = options.cache || new WeakMap()

  if (cache.has(obj)) {
    return cache.get(obj)
  }

  // 函数处理策略
  if (typeof obj === 'function') {
    if (cloneFunction) {
      // 简单的函数克隆(有限制)
      return new Function(`return ${obj.toString()}`)()
    }
    return obj
  }

  // 获取属性的策略
  let keys = Object.keys(obj)

  if (includeSymbols) {
    keys = keys.concat(Object.getOwnPropertySymbols(obj))
  }

  if (includeNonEnumerable) {
    const allKeys = Object.getOwnPropertyNames(obj)
    keys = keys.concat(allKeys.filter(k => !keys.includes(k)))
  }

  const result = Array.isArray(obj) ? [] : {}
  cache.set(obj, result)

  keys.forEach((key) => {
    const descriptor = Object.getOwnPropertyDescriptor(obj, key)

    if (descriptor.get || descriptor.set) {
      // 处理 getter/setter
      Object.defineProperty(result, key, {
        get: descriptor.get,
        set: descriptor.set,
        enumerable: descriptor.enumerable,
        configurable: descriptor.configurable,
      })
    }
    else {
      result[key] = deepCloneWithStrategy(obj[key], {
        ...options,
        currentDepth: currentDepth + 1,
        cache,
      })
    }
  })

  return result
}

3. 流式深拷贝(处理大对象)

javascript
async function streamDeepClone(obj, options = {}) {
  const { chunkSize = 1000, delay = 0 } = options
  const cache = new WeakMap()

  async function cloneChunk(obj, processedCount = 0) {
    if (cache.has(obj)) {
      return cache.get(obj)
    }

    if (typeof obj !== 'object' || obj === null) {
      return obj
    }

    const result = Array.isArray(obj) ? [] : {}
    cache.set(obj, result)

    const keys = Object.keys(obj)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      result[key] = await cloneChunk(obj[key], processedCount + 1)

      // 定期让出控制权
      if (processedCount % chunkSize === 0 && delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay))
      }
    }

    return result
  }

  return await cloneChunk(obj)
}

4. 类型安全的深拷贝

javascript
function typedDeepClone(obj, typeGuards = {}) {
  const cache = new WeakMap()

  function clone(value, expectedType) {
    if (cache.has(value)) {
      return cache.get(value)
    }

    // 类型检查
    if (expectedType && typeGuards[expectedType]) {
      if (!typeGuards[expectedType](value)) {
        throw new TypeError(`Expected ${expectedType}, got ${typeof value}`)
      }
    }

    if (typeof value !== 'object' || value === null) {
      return value
    }

    const result = Array.isArray(value) ? [] : {}
    cache.set(value, result)

    for (const key in value) {
      if (value.hasOwnProperty(key)) {
        result[key] = clone(value[key])
      }
    }

    return result
  }

  return clone(obj)
}

// 使用示例
const typeGuards = {
  user: obj =>
    obj && typeof obj.name === 'string' && typeof obj.age === 'number',
  product: obj =>
    obj && typeof obj.id === 'number' && typeof obj.price === 'number',
}

const user = { name: 'John', age: 30, address: { city: 'NYC' } }
const clonedUser = typedDeepClone(user, typeGuards)

5. 支持原型链的深拷贝

javascript
function deepCloneWithPrototype(obj, cache = new WeakMap()) {
  if (cache.has(obj)) {
    return cache.get(obj)
  }

  if (typeof obj !== 'object' || obj === null) {
    return obj
  }

  // 保持原型链
  const proto = Object.getPrototypeOf(obj)
  const result = Object.create(proto)

  cache.set(obj, result)

  // 复制所有自有属性(包括不可枚举的)
  const propNames = Object.getOwnPropertyNames(obj)
  const propSymbols = Object.getOwnPropertySymbols(obj)

  [...propNames, ...propSymbols].forEach(key => {
    const descriptor = Object.getOwnPropertyDescriptor(obj, key)

    if (descriptor.value !== undefined) {
      descriptor.value = deepCloneWithPrototype(descriptor.value, cache)
    }

    Object.defineProperty(result, key, descriptor)
  })

  return result
}

内容基于 MIT 许可 | 保持节奏 · 持续积累