面试-浅复制和深复制?怎样实现深复制详细解答

😀前言
在前端开发中,我们经常需要“复制”一个对象或数组,比如在修改数据时不想影响原始数据、在 Redux 状态管理中保持数据不可变性、或者在组件中需要生成一个独立的副本。这时就涉及到两个常听到的概念——浅拷贝(Shallow Copy) 和 深拷贝(Deep Copy)。
🏠个人主页:尘觉主页
文章目录
- 浅复制和深复制?怎样实现深复制?
- 一、概念(直观理解)
- 二、常见快捷方法与它们的优缺点
- 三、实现深拷贝的正确思路(要点)
- 四、推荐的 `deepClone` 实现(注释详尽)
- 五、示例验证
- 面试写法简介
- 存在的问题
- 六、常见问题与注意事项(FAQ)
- 七、总结
浅复制和深复制?怎样实现深复制?
一、概念(直观理解)
-
浅拷贝(shallow copy):只复制对象第一层的属性。如果属性值是一个引用类型(比如对象、数组),浅拷贝只复制引用(指向同一块内存)。
例子:const a = { x: 1, y: { z: 2 } }; const b = { ...a }; // 或 Object.assign({}, a) b.y.z = 99; console.log(a.y.z); // 99 —— 因为 a.y 和 b.y 指向同一个对象 -
深拷贝(deep copy / deep clone):把对象的每一层都复制一份,引用类型会被递归复制,最终得到一个完全独立于原对象的新值。修改新对象不会影响原对象。
二、常见快捷方法与它们的优缺点
-
JSON.parse(JSON.stringify(obj))- 优点:写法极简,性能在一些场景下不错。
- 缺点:无法复制函数、
Date(会变成字符串)、RegExp、Map、Set、undefined、Symbol、以及会丢失prototype、不可处理循环引用(会报错)。 - 结论:适用于简单纯 JSON 数据(仅包含对象、数组、数字、字符串、布尔、null)。
-
手写递归拷贝(最常见思路)
- 要解决的问题:类型判断、循环引用、特殊内置类型(Date/RegExp/Map/Set/TypedArray/…)、symbol-key、属性可枚举性/描述符、原型链等。
- 难点:边界情况多,代码需要谨慎。
三、实现深拷贝的正确思路(要点)
- 对象/数组的递归拷贝(区分
Array与普通Object)。 - 处理
null(typeof null === 'object',需特殊判断)。 - 处理常见内置类型:
Date,RegExp,Map,Set(另外还可以支持TypedArray)。 - 使用
WeakMap跟踪已经拷贝过的源对象 => 解决循环引用并避免重复拷贝。 - 复制 Symbol 键 和 不可枚举属性/属性描述符 取决于要求(下面实现会拷贝可枚举键与 Symbol 键;如果要完整复制描述符也可扩展)。
- 函数通常保持原引用(不深拷贝函数体),因为“复制函数”通常没有意义(除非做特殊处理)。
四、推荐的 deepClone 实现(注释详尽)
/*** deepClone - 支持:Object, Array, Date, RegExp, Map, Set, Symbol-key, 循环引用* 不复制:函数的执行上下文(函数保持引用),不会复制原型链上的非自有属性(但保留 __proto__ 指向同一原型)。** 性能/复杂度:时间复杂度与对象总节点数 roughly 成正比(O(n)),但处理 Map/Set/Array 等也会遍历其元素。*/
function isObject(val) {return Object.prototype.toString.call(val) === '[object Object]';
}
function isArray(val) {return Array.isArray(val);
}
function isDate(val) {return Object.prototype.toString.call(val) === '[object Date]';
}
function isRegExp(val) {return Object.prototype.toString.call(val) === '[object RegExp]';
}
function isMap(val) {return Object.prototype.toString.call(val) === '[object Map]';
}
function isSet(val) {return Object.prototype.toString.call(val) === '[object Set]';
}function deepClone(src, map = new WeakMap()) {// 原始类型或函数直接返回(函数按引用处理)if (src === null || typeof src !== 'object') return src;// 已经拷贝过(处理循环引用)if (map.has(src)) return map.get(src);let dst;// 处理 Dateif (isDate(src)) {dst = new Date(src.getTime());map.set(src, dst);return dst;}// 处理 RegExpif (isRegExp(src)) {const flags = src.flags; // g, i, m, u, s, ydst = new RegExp(src.source, flags);map.set(src, dst);return dst;}// 处理 Mapif (isMap(src)) {dst = new Map();map.set(src, dst);for (const [k, v] of src.entries()) {// 键也可能是对象或复杂类型,所以递归克隆键和值const clonedKey = deepClone(k, map);const clonedVal = deepClone(v, map);dst.set(clonedKey, clonedVal);}return dst;}// 处理 Setif (isSet(src)) {dst = new Set();map.set(src, dst);for (const v of src.values()) {dst.add(deepClone(v, map));}return dst;}// 处理 Arrayif (isArray(src)) {dst = [];map.set(src, dst);for (let i = 0; i < src.length; i++) {dst[i] = deepClone(src[i], map);}return dst;}// 处理 TypedArrays(可选扩展 -- 这里给出基本处理)// 例如 Int8Array, Uint8Array, Float32Array 等if (ArrayBuffer.isView(src)) {// 对于 TypedArray 或 DataView,复制底层缓冲区const ctor = src.constructor;dst = new ctor(src);map.set(src, dst);return dst;}// 处理普通对象(包括有 Symbol 键的属性)// 创建新的对象并保留原型(如果你不想保留原型,可改成 {})const proto = Object.getPrototypeOf(src);dst = Object.create(proto);map.set(src, dst);// 获取所有自有属性键:包括字符串键与 Symbol 键const keys = Reflect.ownKeys(src); // 包含不可枚举和可枚举、symbolfor (const key of keys) {// 复制属性描述符(保留 writable/configurable/enumerable/get/set)const desc = Object.getOwnPropertyDescriptor(src, key);if (desc) {if (desc.get || desc.set) {// 如果是 getter/setter,直接定义描述符(保持行为)Object.defineProperty(dst, key, desc);} else {// 普通值:递归拷贝desc.value = deepClone(desc.value, map);Object.defineProperty(dst, key, desc);}}}return dst;
}
五、示例验证
const obj111 = {a: 1,b: {c: 2,d: {e: 3},f: [1, { a: 1, b: 2 }, 3]},t: new Date('2020-01-01'),r: /abc/gi,m: new Map([['k1', { x: 1 }]]),s: new Set([1, 2, { y: 9 }]),[Symbol('sym')]: 'symValue'
};// 循环引用测试
obj111.self = obj111;const cloned = deepClone(obj111);// 验证
console.log(cloned !== obj111); // true
console.log(cloned.b !== obj111.b); // true
console.log(cloned.b.f[1] !== obj111.b.f[1]); // true
console.log(cloned.t instanceof Date && cloned.t.getTime() === obj111.t.getTime()); // true
console.log(cloned.r instanceof RegExp && cloned.r.source === obj111.r.source); // true
console.log(cloned.m instanceof Map && cloned.m.get('k1') !== obj111.m.get('k1')); // true
console.log(cloned.self === cloned); // true (循环引用保持)
面试写法简介
const isObject = (item)=>{return Object.prototype.toString.call(item) === '[object Object]';
}
const isArray = (item)=>{return Object.prototype.toString.call(item) === '[object Array]';
}const deepClone=(obj)=>{const cloneObj=isArray(obj)?[]:isObject(obj)?{}:'';for(let key in obj){if(isObject(obj[key])||isArray(obj[key])){Object.assign(cloneObj,{[key]: deepClone(Reflect.get(obj,key))});}else{cloneObj[key] = obj[key];} }return cloneObj;
}
存在的问题
| 问题点 | 原因说明 | 举例 |
|---|---|---|
① 无法处理 null | 因为 typeof null === 'object',你的 isObject() 会误判 | deepClone({a: null}) 会出错 |
| ② 不能处理其他类型 | 只考虑了数组和对象,像 Date、RegExp、Map、Set 等都拷贝不了 | deepClone({t: new Date()}) 会变成空对象 |
| ③ 没有处理循环引用 | 如果对象自己引用自己,会死循环 | const a={}; a.self=a; deepClone(a) 报错 |
④ 使用 for...in 会遍历原型链上的属性 | 一般我们只想复制对象自身的属性 | |
⑤ 用 Object.assign 每次都创建新对象(性能低) | 其实可以直接 cloneObj[key] = deepClone(obj[key]) | |
⑥ 如果不是对象或数组,返回 '' | 这会造成函数在意外输入时输出错误类型,不如直接返回原值 |
六、常见问题与注意事项(FAQ)
-
为什么不用
for...in?
for...in会遍历原型链上的可枚举属性,通常我们只关心对象自身(自有属性)。上面的实现用Reflect.ownKeys+getOwnPropertyDescriptor来复制自有属性(包括 Symbol 和不可枚举),并保留属性描述符(可扩展为只复制可枚举的属性,视需求而定)。 -
函数如何处理?
函数会当作普通值返回(保持引用)。通常复制函数体并不有意义。如果你确实需要克隆函数(比如绑定上下文或序列化),那是更复杂/不常见的场景。 -
原型链和 constructor?
上面代码通过Object.create(proto)保留了原型。若你想完全保留 constructor、原型上的不可枚举行为或某些特殊行为,需要更复杂的处理。 -
性能
深拷贝比分配引用代价高,若对象非常大或频繁调用,可能影响性能。仅在需要“独立副本”时使用深拷贝。 -
还有哪些类型没处理?
- 几类特殊内置类型(例如
Promise、WeakMap、WeakSet)通常无需克隆或无法克隆(WeakMap/WeakSet 的键是弱引用)。 - DOM 节点、函数闭包环境等无法简单克隆。
- 如果需要克隆属性访问器(get/set 已保留描述符),但若 get 会访问私有闭包状态,克隆无法复制闭包内部状态。
- 几类特殊内置类型(例如
-
如何选择实现?
- 数据简单且只包含 JSON-friendly 类型:
JSON.parse(JSON.stringify(obj))。 - 需要处理循环引用或内置对象:使用上面这种带
WeakMap的手写实现(或使用成熟库,如lodash.cloneDeep,会处理很多边界情况并经过优化)。
- 数据简单且只包含 JSON-friendly 类型:
七、总结
- 先区分“浅拷贝”和“深拷贝”,理解引用类型与原始类型的差别。
- 对简单数据(纯 JSON)可用
JSON.parse(JSON.stringify(...))。 - 需要健壮、通用方案时,使用递归 +
WeakMap来处理循环引用,并对Date/RegExp/Map/Set/TypedArray等做特殊处理。 - 若对性能或边界行为(原型、不可枚举属性、属性描述符)有更细粒度需求,考虑使用并阅读成熟库(例如
lodash的cloneDeep)或根据具体需求扩展实现。
😁热门专栏推荐
想学习vue的可以看看这个
java基础合集
数据库合集
redis合集
nginx合集
linux合集
手写机制
微服务组件
spring_尘觉
springMVC
mybits
等等等还有许多优秀的合集在主页等着大家的光顾感谢大家的支持
🤔欢迎大家加入我的社区 尘觉社区
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😁
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🍻
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🤞

