You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JS对象深克隆:方案问题、瓶颈及原生JS正确实现方法

JavaScript深克隆的完整实现(含非可枚举属性等边缘场景)

嘿,这个问题问到点子上了——深克隆看似简单,但要覆盖所有场景(比如非可枚举属性、循环引用、特殊内置对象)确实容易踩坑。先聊聊你现有代码的思路:用Object.getOwnPropertyNames()获取所有自身属性(包括非可枚举的)是个很棒的起点,但还有不少细节没考虑到,咱们一步步拆解。

你的现有方案存在的瓶颈

  1. 只处理了普通对象,忽略数组/特殊类型:如果传入数组,你的代码会把它当成普通对象克隆,结果变成{}而非数组;像Date、RegExp这类内置对象,直接复制会丢失它们的实例特性。
  2. 未处理循环引用:如果对象存在循环引用(比如user.self = user),递归克隆会导致无限调用,最终栈溢出。
  3. 丢失属性描述符:你直接用cloneObj[key] = ...赋值,会把原属性的enumerable: false、writable等特性覆盖成默认值(默认是可枚举、可写的)。
  4. 遗漏Symbol属性:Object.getOwnPropertyNames()不会返回Symbol类型的键,这类属性会被彻底忽略。
  5. 未处理原型链:克隆后的对象原型默认是Object.prototype,如果原对象是自定义类的实例,会丢失原型上的方法和属性。

原生JS的正确实现方案

下面是一个覆盖绝大多数场景的深克隆函数,我们逐段解释关键逻辑:

function deepClone(obj, cache = new WeakMap()) {
  // 1. 处理基本类型、null、undefined:直接返回原值
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 2. 处理循环引用:如果已经克隆过这个对象,直接返回缓存的克隆结果
  if (cache.has(obj)) {
    return cache.get(obj);
  }

  // 3. 处理特殊内置对象:Date、RegExp
  if (obj instanceof Date) {
    const clone = new Date(obj.getTime());
    cache.set(obj, clone);
    return clone;
  }
  if (obj instanceof RegExp) {
    const clone = new RegExp(obj.source, obj.flags);
    cache.set(obj, clone);
    return clone;
  }

  // 4. 处理函数:这里直接返回原函数(函数克隆意义有限,闭包等场景很难完美复制)
  // 如果需要完全克隆函数,可以尝试new Function,但有安全和功能局限性,这里不展开
  if (typeof obj === 'function') {
    return obj;
  }

  // 5. 创建对应类型的实例:保留原对象的原型链
  const clone = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
  
  // 6. 缓存当前对象,避免后续循环引用
  cache.set(obj, clone);

  // 7. 处理所有自身属性(包括非可枚举):用getOwnPropertyDescriptors获取完整属性描述
  const descriptors = Object.getOwnPropertyDescriptors(obj);
  Object.keys(descriptors).forEach(key => {
    // 递归克隆属性值,并保留原属性的描述符
    Object.defineProperty(clone, key, {
      ...descriptors[key],
      value: deepClone(obj[key], cache)
    });
  });

  // 8. 处理Symbol类型的属性:单独遍历Symbol键
  Object.getOwnPropertySymbols(obj).forEach(symKey => {
    const descriptor = Object.getOwnPropertyDescriptor(obj, symKey);
    Object.defineProperty(clone, symKey, {
      ...descriptor,
      value: deepClone(obj[symKey], cache)
    });
  });

  return clone;
}

关键特性说明

  • 覆盖非可枚举属性:通过Object.getOwnPropertyDescriptors()获取所有属性的完整描述,包括enumerable: false的属性,并用Object.defineProperty()保留这些特性。
  • 解决循环引用:用WeakMap缓存已克隆的对象,既避免栈溢出,又不会造成内存泄漏(WeakMap的键是弱引用,不会阻止垃圾回收)。
  • 保留原型链:用Object.create(Object.getPrototypeOf(obj))创建实例,确保克隆对象继承原对象的原型。
  • 支持特殊类型:单独处理Date、RegExp这类内置对象,保证克隆后的实例能正常工作。
  • 覆盖Symbol属性:通过Object.getOwnPropertySymbols()遍历所有Symbol键,不会遗漏这类属性。

测试你的示例场景

用你提供的user对象测试这个函数:

let user = { 
  name: 'SomeName',
  sayHi: function(){console.log(this.name);}
};
Object.defineProperty(user, 'sayHi', {enumerable: false});

const clonedUser = deepClone(user);
console.log(clonedUser.name); // 输出: SomeName
clonedUser.sayHi(); // 输出: SomeName
// 验证非可枚举特性是否保留
console.log(Object.propertyIsEnumerable.call(clonedUser, 'sayHi')); // 输出: false

这个实现基本能覆盖日常开发中遇到的绝大多数深克隆场景,如果有更特殊的需求(比如克隆Promise、Map/Set等),可以在函数中继续扩展对应的处理逻辑。

内容的提问来源于stack exchange,提问作者Антон Подгорный

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:24:01