手写一版支持 Map/Set/Date/循环引用 的 JavaScript 深拷贝函数

✨ 1. 类型支持

类型 支持
原始类型
数组
对象
Map
Set
Date
循环引用

在前端开发中,我们经常需要拷贝一个对象。有时我们会用:

const copy = JSON.parse(JSON.stringify(obj));

但很快你就会发现:

它会丢失 undefined、Symbol、函数

会报错:TypeError: Converting circular structure to JSON

不支持 Date、Map、Set

那我们能不能手写一份支持多种类型并能处理循环引用的深拷贝函数呢?

这篇文章,我将一步一步带你实现它。


✨ 2. 第一步:基础版 deepClone

我们先从最基础的递归对象拷贝写起:

function isObject(value) {
  return typeof value === 'object' && value !== null;
}

function deepClone(target) {
  if (!isObject(target)) return target;
  const result = Array.isArray(target) ? [] : {};
  for (const key in target) {
    if (Object.prototype.hasOwnProperty.call(target, key)) {
      result[key] = deepClone(target[key]);
    }
  }
  return result;
}

这个函数可以处理对象、数组、嵌套结构,但——它会在遇到循环引用时栈溢出:

const obj = {};
obj.self = obj;

deepClone(obj); // ❌ Maximum call stack size exceeded

我们要想解决这个问题,就要引入缓存。


✨ 3. 第二步:引入 Map 解决循环引用

我们可以使用 Map 来缓存已经拷贝过的对象引用,当遇到重复对象时直接返回:

function deepClone(target) {
  const map = new Map();
  function clone(value) {
    if (map.has(value)) return map.get(value);
    if (!isObject(value)) return value;
    const result = Array.isArray(value) ? [] : {};
    map.set(value, result);
    for (const key in value) {
      if (Object.prototype.hasOwnProperty.call(value, key)) {
        result[key] = clone(value[key]);
      }
    }
    return result;
  }
  return clone(target);
}

这样我们就能正确处理循环结构的对象了。

const obj = {};
obj.self = obj;

deepClone(obj); // ✅  { self: [Circular *1] }

✨ 4. 第三步:支持更多类型(Date、Map、Set、Function)

为了增强实用性,我们还需要支持以下类型:

  • Date:保留时间值
  • Map / Set:递归遍历 key/value 或集合
  • Function:直接返回原函数

这是最终版:

/**
 * 判断一个值是否是非 null 的对象类型(包括对象、数组、Map、Set 等)。
 *
 * 注意:null 也属于 typeof === 'object',但不是有效对象,因此需要额外判断。
 *
 * @param {any} value - 要判断的值
 * @returns {boolean} 如果是非 null 的对象则返回 true,否则返回 false
 */
function isObject(value) {
  return typeof value === 'object' && value !== null;
}

/**
 * 深拷贝函数:支持对象、数组、Date、Map、Set 等类型,自动处理循环引用。
 *
 * @param {any} target - 要深拷贝的目标值
 * @returns {any} 返回深拷贝后的新对象
 */
function deepClone(target) {
  // 缓存已拷贝对象,防止循环引用导致栈溢出
  const map = new Map();
  /**
   * 递归克隆函数
   * @param {any} _value - 当前处理的值
   * @returns {any} 拷贝后的值
   */
  function clone(_value) {
    // 如果是已经处理过的对象,直接返回缓存结果(防止循环引用)
    if (map.has(_value)) return map.get(_value);

    // 原始类型(number、string、boolean、null、undefined、symbol)直接返回
    if (!isObject(_value)) return _value;

    // 函数类型不做处理,直接返回引用本身
    if (typeof _value === 'function') {
      map.set(_value, _value);
      return _value;
    }

    let result;

    // Date 类型
    if (_value instanceof Date) {
      result = new Date(_value);
      map.set(_value, result);
      return result;
    }

    // Map 类型
    if (_value instanceof Map) {
      result = new Map();
      map.set(_value, result);
      _value.forEach((val, key) => {
        result.set(clone(key), clone(val));
      });
      return result;
    }

    // Set 类型
    if (_value instanceof Set) {
      result = new Set();
      map.set(_value, result);
      _value.forEach((item) => {
        result.add(clone(item));
      });
      return result;
    }

    // 普通对象或数组
    result = Array.isArray(_value) ? [] : {};
    map.set(_value, result);

    // 递归处理对象自身的可枚举属性(不包括原型链上的)
    for (const key in _value) {
      if (Object.hasOwn(_value, key)) {
        result[key] = clone(_value[key]);
      }
    }

    return result;
  }

  return clone(target);
}

测试用例

let obj = {
  a: 1,
  b: [1, 2, 3],
  c: new Map([['a', 123]]),
  d: new Set([{ a: 1 }, 2, 3]),
  e: function () {
    return this.a;
  },
  f: new Date('2019-10-05T14:00:00'),
};
// 制造循环引用
obj.a = obj;

const result = deepClone(obj);

console.log('原对象:', obj);
console.log('深拷贝结果:', result);

// 验证对象是否保持一致
console.log('obj.b === result.b:', obj.b === result.b); // false

// 验证循环引用是否保持一致
console.log('obj.a === obj:', obj.a === obj); // true
console.log('result.a === result:', result.a === result); // true

// 验证函数是否被正确拷贝(引用保持一致)
console.log('函数调用 result.e():', result.e()); // 应该返回 result.a 的值

// 验证 Map 拷贝
console.log('Map 类型:', result.c instanceof Map, Array.from(result.c.entries()));

// 验证 Set 拷贝
console.log('Set 类型:', result.d instanceof Set, Array.from(result.d));

// 验证 Date 拷贝
console.log('Date 类型:', result.f instanceof Date, result.f.toISOString());

✨ 5. 总结:可用性、扩展建议
我们已经实现了一个非常完善且稳定的深拷贝函数

当然还有更多可以支持的扩展:

  • RegExp、Error 类型处理
  • 保留对象原型链(Object.create)
  • 属性描述符(defineProperty)

你可以按需继续增强它,最终变成你自己的 deepClone 工具函数。

感谢你的阅读!
希望这篇文章能帮你更好地理解深拷贝的奥秘,写出更健壮的代码。
如果喜欢,别忘了点个赞,收藏✨,转发,让我知道你的支持!

下次再见,我们一起探索更多有趣的技术世界!
祝你编码顺利,Bug 远离!✨

你可能感兴趣的:(javascript前端深拷贝)