JS对象深克隆:方案问题、瓶颈及原生JS正确实现方法
JavaScript深克隆的完整实现(含非可枚举属性等边缘场景)
嘿,这个问题问到点子上了——深克隆看似简单,但要覆盖所有场景(比如非可枚举属性、循环引用、特殊内置对象)确实容易踩坑。先聊聊你现有代码的思路:用Object.getOwnPropertyNames()获取所有自身属性(包括非可枚举的)是个很棒的起点,但还有不少细节没考虑到,咱们一步步拆解。
你的现有方案存在的瓶颈
- 只处理了普通对象,忽略数组/特殊类型:如果传入数组,你的代码会把它当成普通对象克隆,结果变成
{}而非数组;像Date、RegExp这类内置对象,直接复制会丢失它们的实例特性。 - 未处理循环引用:如果对象存在循环引用(比如
user.self = user),递归克隆会导致无限调用,最终栈溢出。 - 丢失属性描述符:你直接用
cloneObj[key] = ...赋值,会把原属性的enumerable: false、writable等特性覆盖成默认值(默认是可枚举、可写的)。 - 遗漏Symbol属性:
Object.getOwnPropertyNames()不会返回Symbol类型的键,这类属性会被彻底忽略。 - 未处理原型链:克隆后的对象原型默认是
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,提问作者Антон Подгорный
相关产品推荐
相关产品推荐

