
😀前言
在前端开发中,我们经常需要"复制"一个对象或数组,比如在修改数据时不想影响原始数据、在 Redux 状态管理中保持数据不可变性、或者在组件中需要生成一个独立的副本。这时就涉及到两个常听到的概念------浅拷贝(Shallow Copy) 和 深拷贝(Deep Copy)。
🏠个人主页:尘觉主页
文章目录
- 浅复制和深复制?怎样实现深复制?
-
- 一、概念(直观理解)
- 二、常见快捷方法与它们的优缺点
- 三、实现深拷贝的正确思路(要点)
- [四、推荐的 `deepClone` 实现(注释详尽)](#四、推荐的
deepClone实现(注释详尽)) - 五、示例验证
- 六、常见问题与注意事项(FAQ)
- 七、总结
浅复制和深复制?怎样实现深复制?
一、概念(直观理解)
-
浅拷贝(shallow copy) :只复制对象第一层的属性。如果属性值是一个引用类型(比如对象、数组),浅拷贝只复制引用(指向同一块内存)。
例子:
jsconst 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 实现(注释详尽)
js
/**
* 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;
// 处理 Date
if (isDate(src)) {
dst = new Date(src.getTime());
map.set(src, dst);
return dst;
}
// 处理 RegExp
if (isRegExp(src)) {
const flags = src.flags; // g, i, m, u, s, y
dst = new RegExp(src.source, flags);
map.set(src, dst);
return dst;
}
// 处理 Map
if (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;
}
// 处理 Set
if (isSet(src)) {
dst = new Set();
map.set(src, dst);
for (const v of src.values()) {
dst.add(deepClone(v, map));
}
return dst;
}
// 处理 Array
if (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); // 包含不可枚举和可枚举、symbol
for (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;
}
五、示例验证
js
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 (循环引用保持)
面试写法简介
js
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的可以看看这个
等等等还有许多优秀的合集在主页等着大家的光顾感谢大家的支持
🤔欢迎大家加入我的社区 尘觉社区
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😁
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🍻
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🤞
