)
1. 面试题题目题目请手写一个完美的 JavaScript Deep Clone深拷贝函数。考察点数据类型体系对原始类型、引用类型、特殊对象Date, RegExp, Map, Set, Symbol的理解。内存与引用机制堆栈存储区别、浅拷贝与深拷贝的本质差异。递归与算法递归遍历、环检测循环引用处理。API熟练度Object.prototype.toString.call、Reflect.ownKeys、WeakMap弱引用特性。工程化思维边界条件处理、性能考量、生产环境最佳实践。2. 核心思路一句话以“精确类型判断”为基础利用“WeakMap解决循环引用”通过“递归分类构造”实现全类型独立副本创建并在生产环境中优先使用原生结构化克隆算法。3. 解决方案架构图文本版[输入: Target] │ ▼ ┌─────────────────┐ │ 1. 基础校验 │ ──► 原始类型/Null/Function ──► 直接返回 (或特殊处理) └────────┬────────┘ ▼ ┌─────────────────┐ │ 2. 循环引用检测 │ ──► WeakMap.has(Target) ? │ (Memoization) │ ├── YES: 返回缓存的 CloneTarget (阻断递归) └────────┬────────┘ └── NO: 继续向下 ▼ ┌─────────────────┐ │ 3. 精确类型获取 │ ──► Object.prototype.toString.call(Target) └────────┬────────┘ ▼ ┌─────────────────┐ │ 4. 初始化容器 │ │ 缓存写入 │ ──► new Date/RegExp/Map/Set/Array/Object │ (关键:先写缓存) │ ──► WeakMap.set(Target, CloneTarget) ⚠️必须在递归前 └────────┬────────┘ ▼ ┌─────────────────┐ │ 5. 内容填充 │ │ (分类递归) │ │ ├─ Array/Object │ ──► Reflect.ownKeys 递归赋值 │ ├─ Map │ ──► forEach 递归 set │ ├─ Set │ ──► forEach 递归 add │ └─ Date/RegExp │ ──► 已在步骤4完成值拷贝 └────────┬────────┘ ▼ [输出: CloneTarget]4. 核心干货与矛盾分析主要矛盾 vs 次要矛盾主要矛盾数据完整性与独立性。即如何确保克隆出的对象在内存上完全独立且保留所有类型特征包括不可枚举属性、Symbol键、循环结构。次要矛盾性能与兼容性。递归带来的调用栈开销、WeakMap的浏览器兼容性、以及是否需要支持DOM节点/Blob等非纯JS对象。关键知识点更正与补充JSON方案的致命缺陷更正无法处理undefined、Function、Symbol作为value时丢失作为key时被忽略。Date变为字符串RegExp/Error变为空对象{}。Map/Set变为空对象{}。循环引用直接抛出异常。原型链丢失所有对象变成纯Object。WeakMap 而非 Map补充原理必须使用WeakMap而不是Map。因为Map是强引用如果克隆函数执行完毕但外部仍持有Map引用原对象将无法被垃圾回收GC导致内存泄漏。WeakMap的键是弱引用当原对象无其他引用时GC会自动回收该键值对。缓存写入时机易错点必须先map.set(target, cloneTarget)再递归填充属性。如果在递归之后才写入遇到循环引用时递归进入子属性发现父对象未缓存依然会无限递归导致栈溢出。Symbol 键的处理补充for...in和Object.keys()都无法获取 Symbol 键。必须使用Reflect.ownKeys()同时返回字符串键和Symbol键或Object.getOwnPropertySymbols()。更好的解决方案生产环境补充structuredClone()现代浏览器/Node.js v17 原生支持。能处理循环引用、Map/Set/Date/RegExp/ArrayBuffer/Blob/File等。缺点不支持Function、DOM节点、原型链。lodash.cloneDeep工业级标准处理了Buffer、TypedArray、Arguments等极端边界。5. 使用场景与边界场景场景分类具体描述推荐方案注意事项简单配置合并纯JSON数据无特殊类型JSON.parse(JSON.stringify())确认无undefined/function/循环引用状态管理快照Redux/Vuex中保存历史状态structuredClone()或 lodash需保证状态树不含Function表单数据编辑编辑时暂存原始数据用于取消还原手写Deep Clone / structuredClone需保留Date/RegExp等类型复杂对象序列化包含Function/原型链/循环引用手写Deep Clone (完整版)面试高频考点跨线程通信Worker/postMessagestructuredClone (内置)浏览器自动使用结构化克隆算法DOM/Window克隆包含浏览器宿主对象❌ 禁止深拷贝DOM节点不应被深拷贝应使用cloneNode6. 完整示例代码含详细注释/** * 精确获取数据类型 * param {*} value * returns {string} 如 [object Date], [object Map] */functiongetType(value){returnObject.prototype.toString.call(value);}/** * 判断是否为可遍历的引用类型需要递归处理 * param {string} type * returns {boolean} */functionisIterableType(type){constiterableTypes[[object Object],[object Array],[object Map],[object Set]];returniterableTypes.includes(type);}/** * 完美深拷贝函数 * param {*} target - 要克隆的目标 * param {WeakMap} cache - 循环引用缓存内部使用外部勿传 * returns {*} 克隆后的新对象 */functiondeepClone(target,cachenewWeakMap()){// 1. 基础类型 null function 直接返回 // typeof null object所以必须显式判断null// Function 通常不需要克隆共享引用即可若需克隆需用new Function()但有安全风险if(targetnull||typeoftarget!object){returntarget;}// 2. 循环引用检测 // 如果已经克隆过该对象直接返回缓存的副本避免无限递归if(cache.has(target)){returncache.get(target);}// 3. 精确类型判断 初始化容器 consttypegetType(target);letcloneTarget;switch(type){case[object Date]:// Date: 通过时间戳创建新实例cloneTargetnewDate(target.getTime());break;case[object RegExp]:// RegExp: 复制source和flagscloneTargetnewRegExp(target.source,target.flags);break;case[object Error]:// Error: 复制message和stackcloneTargetnewError(target.message);cloneTarget.stacktarget.stack;break;case[object Map]:cloneTargetnewMap();break;case[object Set]:cloneTargetnewSet();break;case[object Array]:cloneTarget[];break;case[object Object]:// 保留原型链使用Object.create保持与原对象相同的原型cloneTargetObject.create(Object.getPrototypeOf(target));break;default:// ArrayBuffer, TypedArray, WeakMap, WeakSet 等特殊类型// 此处简化处理生产环境建议参考lodash或使用structuredClonereturntarget;}// 4. ⚠️ 关键立即写入缓存在递归之前// 防止后续递归中遇到循环引用时找不到缓存cache.set(target,cloneTarget);// 5. 分类填充内容 switch(type){case[object Map]:target.forEach((value,key){// Map的key和value都需要深拷贝cloneTarget.set(deepClone(key,cache),deepClone(value,cache));});break;case[object Set]:target.forEach((value){cloneTarget.add(deepClone(value,cache));});break;case[object Array]:case[object Object]:// 使用 Reflect.ownKeys 同时获取// 1. 字符串键包括不可枚举// 2. Symbol键// 比 for...in getOwnPropertySymbols 更简洁完整constkeysReflect.ownKeys(target);keys.forEach((key){// 获取属性描述符保留 configurable/enumerable/writable/get/setconstdescriptorObject.getOwnPropertyDescriptor(target,key);if(descriptor){if(valueindescriptor){// 普通数据属性递归克隆valuedescriptor.valuedeepClone(descriptor.value,cache);}// accessor属性(get/set)不递归直接复制描述符Object.defineProperty(cloneTarget,key,descriptor);}});break;}returncloneTarget;}7. 满分答案总结面试口述版面试官您好关于手写Deep Clone我的回答分为三个层次第一层为什么不能用JSONJSON序列化方案存在四大硬伤丢失undefined/Symbol/Function、Date变字符串、RegExp/Set/Map变空对象、循环引用直接报错。它只适用于纯数据序列化不能作为通用深拷贝方案。第二层手写实现的核心三要素循环引用使用WeakMap作为备忘录利用其弱引用特性避免内存泄漏。关键点是在创建空容器后、递归填充前就立即写入缓存否则循环引用仍会导致栈溢出。类型完备性用Object.prototype.toString.call精确识别类型针对Date/RegExp/Map/Set/Error分别用构造函数重建而非统一当作Object处理。属性完整性使用Reflect.ownKeys替代for...in确保Symbol键和不可枚举属性都被拷贝同时通过getOwnPropertyDescriptor保留属性的元信息writable/configurable等。第三层生产环境的最佳实践在实际工程中我不会重复造轮子纯数据场景优先用structuredClone()它是浏览器原生结构化克隆算法支持循环引用和大部分内置类型性能优于JS递归。需要支持Function/原型链/Buffer等极端场景时使用lodash.cloneDeep。只有在面试或极简依赖场景下才会使用上述手写方案。以上是我对Deep Clone从原理到实践的完整理解。