跳到主要内容

深拷贝与浅拷贝

问题

手写实现浅拷贝和深拷贝,要求能处理循环引用、特殊对象类型等边界情况。

答案

拷贝是将一个对象的值复制到另一个对象。浅拷贝只复制第一层属性,嵌套对象仍共享引用;深拷贝递归复制所有层级,完全独立。


浅拷贝

概念

浅拷贝只复制对象的第一层属性,如果属性值是引用类型,则复制的是引用地址。

实现方式

// 方式1:Object.assign
function shallowCopy1<T extends object>(obj: T): T {
return Object.assign({}, obj);
}

// 方式2:展开运算符
function shallowCopy2<T extends object>(obj: T): T {
return { ...obj } as T;
}

// 方式3:手写实现
function shallowCopy<T extends object>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}

// 处理数组
if (Array.isArray(obj)) {
return [...obj] as unknown as T;
}

// 处理普通对象
const result = {} as T;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = obj[key];
}
}
return result;
}

// 测试
const original = {
a: 1,
b: { c: 2 },
d: [1, 2, 3],
};

const copied = shallowCopy(original);
console.log(copied.a === original.a); // true
console.log(copied.b === original.b); // true(共享引用)
console.log(copied.d === original.d); // true(共享引用)

数组浅拷贝

const arr = [1, 2, { a: 3 }];

// 方式1:slice
const copy1 = arr.slice();

// 方式2:concat
const copy2 = arr.concat();

// 方式3:展开运算符
const copy3 = [...arr];

// 方式4:Array.from
const copy4 = Array.from(arr);

深拷贝

基础版(JSON 方法)

// 最简单但有局限性
function deepCloneJSON<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}

// 局限性:
// 1. 无法处理 undefined、Symbol、函数
// 2. 无法处理循环引用
// 3. 无法处理 Date、RegExp、Map、Set 等特殊对象
// 4. 会丢失对象的原型链

递归版(基础)

function deepClone<T>(obj: T): T {
// 处理基本类型和 null
if (obj === null || typeof obj !== 'object') {
return obj;
}

// 处理数组
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item)) as unknown as T;
}

// 处理普通对象
const result = {} as T;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = deepClone(obj[key]);
}
}
return result;
}

完整版(处理循环引用 + 特殊类型)

function deepClone<T>(obj: T, hash = new WeakMap()): T {
// 处理基本类型和 null
if (obj === null || typeof obj !== 'object') {
return obj;
}

// 处理循环引用
if (hash.has(obj as object)) {
return hash.get(obj as object);
}

// 处理 Date
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T;
}

// 处理 RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags) as unknown as T;
}

// 处理 Map
if (obj instanceof Map) {
const result = new Map();
hash.set(obj as object, result);
obj.forEach((value, key) => {
result.set(deepClone(key, hash), deepClone(value, hash));
});
return result as unknown as T;
}

// 处理 Set
if (obj instanceof Set) {
const result = new Set();
hash.set(obj as object, result);
obj.forEach((value) => {
result.add(deepClone(value, hash));
});
return result as unknown as T;
}

// 处理数组
if (Array.isArray(obj)) {
const result: unknown[] = [];
hash.set(obj as object, result);
obj.forEach((item, index) => {
result[index] = deepClone(item, hash);
});
return result as unknown as T;
}

// 处理普通对象(保持原型链)
const result = Object.create(Object.getPrototypeOf(obj));
hash.set(obj as object, result);

// 处理 Symbol 键
const symbolKeys = Object.getOwnPropertySymbols(obj);
symbolKeys.forEach((symKey) => {
result[symKey] = deepClone((obj as Record<symbol, unknown>)[symKey], hash);
});

// 处理普通键
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = deepClone(obj[key], hash);
}
}

return result;
}

终极版(支持更多类型)

type Cloneable =
| null
| undefined
| boolean
| number
| string
| symbol
| bigint
| Date
| RegExp
| Map<unknown, unknown>
| Set<unknown>
| Array<unknown>
| ArrayBuffer
| DataView
| Int8Array
| Uint8Array
| Float32Array
| Float64Array
| object;

function deepCloneUltimate<T extends Cloneable>(
obj: T,
hash = new WeakMap()
): T {
// 处理基本类型
if (obj === null || typeof obj !== 'object') {
return obj;
}

// 处理循环引用
if (hash.has(obj as object)) {
return hash.get(obj as object);
}

// 获取对象类型
const type = Object.prototype.toString.call(obj);

// 类型处理器映射
const handlers: Record<string, (value: unknown) => unknown> = {
'[object Date]': (v) => new Date((v as Date).getTime()),
'[object RegExp]': (v) => {
const r = v as RegExp;
return new RegExp(r.source, r.flags);
},
'[object Error]': (v) => {
const e = v as Error;
const newError = new Error(e.message);
newError.name = e.name;
newError.stack = e.stack;
return newError;
},
'[object ArrayBuffer]': (v) => (v as ArrayBuffer).slice(0),
'[object DataView]': (v) => {
const dv = v as DataView;
return new DataView(dv.buffer.slice(0));
},
'[object Int8Array]': (v) => new Int8Array(v as Int8Array),
'[object Uint8Array]': (v) => new Uint8Array(v as Uint8Array),
'[object Uint8ClampedArray]': (v) => new Uint8ClampedArray(v as Uint8ClampedArray),
'[object Int16Array]': (v) => new Int16Array(v as Int16Array),
'[object Uint16Array]': (v) => new Uint16Array(v as Uint16Array),
'[object Int32Array]': (v) => new Int32Array(v as Int32Array),
'[object Uint32Array]': (v) => new Uint32Array(v as Uint32Array),
'[object Float32Array]': (v) => new Float32Array(v as Float32Array),
'[object Float64Array]': (v) => new Float64Array(v as Float64Array),
'[object BigInt64Array]': (v) => new BigInt64Array(v as BigInt64Array),
'[object BigUint64Array]': (v) => new BigUint64Array(v as BigUint64Array),
};

// 处理特殊类型
if (handlers[type]) {
return handlers[type](obj) as T;
}

// 处理 Map
if (type === '[object Map]') {
const result = new Map();
hash.set(obj as object, result);
(obj as Map<unknown, unknown>).forEach((value, key) => {
result.set(
deepCloneUltimate(key as Cloneable, hash),
deepCloneUltimate(value as Cloneable, hash)
);
});
return result as T;
}

// 处理 Set
if (type === '[object Set]') {
const result = new Set();
hash.set(obj as object, result);
(obj as Set<unknown>).forEach((value) => {
result.add(deepCloneUltimate(value as Cloneable, hash));
});
return result as T;
}

// 处理数组
if (Array.isArray(obj)) {
const result: unknown[] = [];
hash.set(obj, result);
obj.forEach((item, index) => {
result[index] = deepCloneUltimate(item as Cloneable, hash);
});
return result as T;
}

// 处理普通对象
const result = Object.create(Object.getPrototypeOf(obj));
hash.set(obj as object, result);

// 获取所有属性(包括不可枚举和 Symbol)
const allKeys = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj),
];

allKeys.forEach((key) => {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor) {
if (descriptor.value !== undefined) {
descriptor.value = deepCloneUltimate(descriptor.value as Cloneable, hash);
}
Object.defineProperty(result, key, descriptor);
}
});

return result;
}

测试用例

// 测试循环引用
const circularObj: Record<string, unknown> = { a: 1 };
circularObj.self = circularObj;
const clonedCircular = deepClone(circularObj);
console.log(clonedCircular.self === clonedCircular); // true
console.log(clonedCircular !== circularObj); // true

// 测试特殊类型
const specialObj = {
date: new Date(),
regex: /test/gi,
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
nested: { a: { b: { c: 1 } } },
arr: [1, [2, [3]]],
};

const clonedSpecial = deepClone(specialObj);
console.log(clonedSpecial.date instanceof Date); // true
console.log(clonedSpecial.date !== specialObj.date); // true
console.log(clonedSpecial.regex instanceof RegExp); // true
console.log(clonedSpecial.map instanceof Map); // true
console.log(clonedSpecial.set instanceof Set); // true

各方法对比

方法循环引用特殊类型函数Symbol性能
JSON.parse/stringify
递归基础版
WeakMap + 递归
structuredClone部分支持

structuredClone(原生 API)

// 现代浏览器原生深拷贝
const obj = { a: 1, b: { c: 2 }, date: new Date() };
const cloned = structuredClone(obj);

// 支持:
// - 循环引用
// - Date、RegExp、Map、Set、ArrayBuffer 等
// 不支持:
// - 函数、Symbol、DOM 节点

常见面试问题

Q1: 浅拷贝和深拷贝的区别?

答案

对比项浅拷贝深拷贝
复制层级只复制第一层递归复制所有层
引用类型共享引用独立副本
原对象影响修改会互相影响完全独立
性能
实现方式Object.assign、展开运算符递归、JSON、structuredClone

Q2: JSON.parse(JSON.stringify()) 的局限性?

答案

const obj = {
fn: function () {}, // ❌ 丢失
sym: Symbol('test'), // ❌ 丢失
undef: undefined, // ❌ 丢失
date: new Date(), // ❌ 变成字符串
regex: /test/, // ❌ 变成空对象 {}
nan: NaN, // ❌ 变成 null
infinity: Infinity, // ❌ 变成 null
// 循环引用会报错
};

const cloned = JSON.parse(JSON.stringify(obj));
console.log(cloned.fn); // undefined
console.log(cloned.date); // "2024-01-01T00:00:00.000Z"(字符串)
console.log(cloned.regex); // {}

Q3: 如何处理循环引用?

答案

function deepClone<T>(obj: T, hash = new WeakMap()): T {
if (typeof obj !== 'object' || obj === null) return obj;

// 检查是否已经克隆过
if (hash.has(obj as object)) {
return hash.get(obj as object);
}

const result = Array.isArray(obj) ? [] : {};
// 先存入 hash,再递归
hash.set(obj as object, result);

for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
(result as Record<string, unknown>)[key] = deepClone(
obj[key],
hash
);
}
}

return result as T;
}

Q4: 为什么用 WeakMap 而不是 Map?

答案

// WeakMap 的键是弱引用
// 当原对象被垃圾回收时,WeakMap 中的条目也会被清除
// 避免内存泄漏

const hash = new WeakMap();
let obj: object | null = { a: 1 };
hash.set(obj, 'cloned');

obj = null; // 原对象可以被垃圾回收
// WeakMap 中的条目也会被自动清除

Q5: structuredClone 是什么?它和 JSON.parse(JSON.stringify()) 有什么区别?

答案

structuredClone 是浏览器提供的原生深拷贝 API(2022 年起主流浏览器全面支持),基于结构化克隆算法实现。

基本用法

const original = {
name: 'Alice',
date: new Date(),
pattern: /test/gi,
data: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
nested: { a: { b: { c: 1 } } },
};

// 一行代码实现深拷贝
const cloned = structuredClone(original);

// 完全独立的副本
cloned.nested.a.b.c = 999;
console.log(original.nested.a.b.c); // 1(不受影响)

与 JSON.parse(JSON.stringify()) 的对比

特性structuredCloneJSON.parse(JSON.stringify())
循环引用支持报错 TypeError
Date保持 Date 对象变成字符串
RegExp保持 RegExp 对象变成空对象 {}
Map / Set支持丢失,变成 {}
ArrayBuffer / TypedArray支持不支持
Error支持丢失
undefined保留丢失
NaN / Infinity保留变成 null
函数不支持,抛 DataCloneError丢失(静默忽略)
Symbol不支持,抛 DataCloneError丢失(静默忽略)
DOM 节点不支持不支持
原型链不保留(变成普通对象)不保留
性能较快(原生实现)中等(序列化 + 反序列化)
// 循环引用:structuredClone 支持,JSON 不支持
const obj: Record<string, unknown> = { a: 1 };
obj.self = obj;

const cloned1 = structuredClone(obj);
console.log(cloned1.self === cloned1); // true(正确处理)

// JSON.parse(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON

// Date 对象
const withDate = { date: new Date('2024-01-01') };
const jsonClone = JSON.parse(JSON.stringify(withDate));
console.log(jsonClone.date instanceof Date); // false(变成字符串了)
console.log(structuredClone(withDate).date instanceof Date); // true

// 函数:两者都不支持,但行为不同
const withFn = { fn: () => 'hello', name: 'test' };
// structuredClone(withFn); // 抛出 DataCloneError
JSON.parse(JSON.stringify(withFn)); // { name: 'test' }(fn 被静默丢弃)
兼容性

structuredClone 在以下环境中可用:

  • Chrome 98+、Firefox 94+、Safari 15.4+、Edge 98+
  • Node.js 17+(全局可用),Node.js 11+(通过 v8 模块)
  • 对于不支持的环境,可使用 core-js polyfill

Q6: 如何处理深拷贝中的循环引用?

答案

循环引用是指对象的某个属性直接或间接引用了自身。如果不处理循环引用,递归深拷贝会导致无限递归,最终栈溢出。

解决方案:使用 WeakMap 缓存已拷贝的对象

function deepClone<T>(obj: T, hash = new WeakMap()): T {
// 基本类型直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}

// 关键:检查是否已经拷贝过该对象
if (hash.has(obj as object)) {
return hash.get(obj as object); // 直接返回缓存的拷贝
}

// 创建新对象
const result = Array.isArray(obj)
? [] as unknown as T
: Object.create(Object.getPrototypeOf(obj));

// 关键:先缓存,再递归(顺序不能反!)
hash.set(obj as object, result);

// 递归拷贝属性
for (const key of Reflect.ownKeys(obj as object)) {
(result as Record<string | symbol, unknown>)[key] = deepClone(
(obj as Record<string | symbol, unknown>)[key],
hash
);
}

return result;
}

测试循环引用

// 直接循环引用
const obj: Record<string, unknown> = { name: 'root' };
obj.self = obj;

const cloned = deepClone(obj);
console.log(cloned.self === cloned); // true(正确指向拷贝后的自身)
console.log(cloned.self !== obj); // true(不是原对象)
console.log(cloned !== obj); // true(独立副本)

// 间接循环引用
const a: Record<string, unknown> = { name: 'a' };
const b: Record<string, unknown> = { name: 'b' };
a.ref = b;
b.ref = a; // a → b → a 形成环

const clonedA = deepClone(a);
console.log(clonedA.ref !== b); // true
console.log((clonedA.ref as typeof a).ref === clonedA); // true(环形结构保留)
为什么先缓存再递归?

如果先递归再缓存,遇到循环引用时递归还没结束就又进入了,导致无限循环。必须在创建新对象后立即将映射关系存入 WeakMap,这样递归遇到同一个对象时就能直接返回。

Q7: 深拷贝需要处理哪些特殊类型?如何处理?

答案

一个完善的深拷贝需要针对不同类型采用不同的复制策略:

function deepClone<T>(obj: T, hash = new WeakMap()): T {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj as object)) return hash.get(obj as object);

let result: any;

// 1. Date → 使用 getTime() 创建新实例
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T;
}

// 2. RegExp → 使用 source 和 flags 创建新实例
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags) as unknown as T;
}

// 3. Map → 遍历键值对,递归拷贝
if (obj instanceof Map) {
result = new Map();
hash.set(obj as object, result);
obj.forEach((value, key) => {
result.set(deepClone(key, hash), deepClone(value, hash));
});
return result as T;
}

// 4. Set → 遍历元素,递归拷贝
if (obj instanceof Set) {
result = new Set();
hash.set(obj as object, result);
obj.forEach((value) => {
result.add(deepClone(value, hash));
});
return result as T;
}

// 5. ArrayBuffer → 使用 slice 创建副本
if (obj instanceof ArrayBuffer) {
return obj.slice(0) as unknown as T;
}

// 6. TypedArray → 基于 buffer 的 slice 创建
if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
const TypedArrayConstructor = obj.constructor as new (
buffer: ArrayBuffer
) => typeof obj;
return new TypedArrayConstructor(
obj.buffer.slice(0)
) as unknown as T;
}

// 7. Error → 创建同类型的新 Error
if (obj instanceof Error) {
const ErrorConstructor = obj.constructor as new (message: string) => Error;
const newError = new ErrorConstructor(obj.message);
newError.name = obj.name;
newError.stack = obj.stack;
return newError as unknown as T;
}

// 8. 数组
if (Array.isArray(obj)) {
result = [];
hash.set(obj, result);
obj.forEach((item, index) => {
result[index] = deepClone(item, hash);
});
return result as T;
}

// 9. 普通对象(保持原型链)
result = Object.create(Object.getPrototypeOf(obj));
hash.set(obj as object, result);

// 10. 处理 Symbol 键和普通键
for (const key of Reflect.ownKeys(obj as object)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor) {
if ('value' in descriptor) {
descriptor.value = deepClone(descriptor.value, hash);
}
Object.defineProperty(result, key, descriptor);
}
}

return result;
}

各类型处理方式汇总

类型处理方式说明
Datenew Date(obj.getTime())用时间戳创建新实例
RegExpnew RegExp(obj.source, obj.flags)用源码和标志创建新实例
Map遍历 + 递归拷贝键值键和值都需要深拷贝
Set遍历 + 递归拷贝元素每个元素都需要深拷贝
ArrayBufferobj.slice(0)原生方法创建副本
TypedArray基于新 buffer 创建Int8Array、Float32Array 等
Errornew Error(msg) + 复制属性保留 name、message、stack
Array遍历 + 递归按索引逐个深拷贝
ObjectObject.create(proto) + 遍历保持原型链
Symbol 键Reflect.ownKeys 获取for...in 无法遍历 Symbol
// 测试各种类型
const complex = {
date: new Date('2024-01-01'),
regex: /hello/gi,
map: new Map<string, number[]>([['nums', [1, 2, 3]]]),
set: new Set([{ a: 1 }, { b: 2 }]),
buffer: new ArrayBuffer(8),
error: new TypeError('test error'),
[Symbol('key')]: 'symbol value',
};

const cloned = deepClone(complex);

// 验证独立性
console.log(cloned.date !== complex.date); // true
console.log(cloned.date.getTime() === complex.date.getTime()); // true
console.log(cloned.regex !== complex.regex); // true
console.log(cloned.regex.source === complex.regex.source); // true
console.log(cloned.map !== complex.map); // true
console.log(cloned.map.get('nums') !== complex.map.get('nums')); // true(深拷贝)

Q8: 实际项目中你会用什么方案做深拷贝?各方案的优缺点?

答案

方案优点缺点适用场景
structuredClone原生 API、性能好、支持循环引用和大多数类型不支持函数/Symbol/DOM、IE 不支持首选方案,现代项目
lodash.cloneDeep功能最全面、处理各种边界情况、久经考验需要引入依赖、包体积增大需要处理复杂类型、兼容旧浏览器
JSON 序列化零依赖、一行代码、简单直观不支持循环引用/函数/Date/RegExp/Map/Set简单数据对象、配置对象
手写递归完全可控、可定制容易遗漏边界情况、需要维护面试、有特殊需求

推荐选择顺序

// 1. 首选 structuredClone(现代项目)
const cloned1 = structuredClone(data);

// 2. 需要兼容旧环境或处理函数 → lodash
import cloneDeep from 'lodash/cloneDeep'; // 按需引入,减小体积
const cloned2 = cloneDeep(data);

// 3. 纯数据对象(无特殊类型) → JSON
const cloned3 = JSON.parse(JSON.stringify(data));

// 4. 有特殊需求 → 手写
const cloned4 = deepClone(data);

实际项目中的最佳实践

// 封装一个通用的深拷贝工具函数
function clone<T>(value: T): T {
// 基本类型直接返回
if (value === null || typeof value !== 'object') {
return value;
}

// 优先使用 structuredClone
if (typeof structuredClone === 'function') {
try {
return structuredClone(value);
} catch {
// structuredClone 不支持的类型(如函数),降级处理
}
}

// 降级:使用自定义深拷贝
return deepClone(value);
}

// 对于 Redux/状态管理中的不可变更新,推荐使用 Immer
import { produce } from 'immer';

const nextState = produce(state, (draft) => {
draft.user.name = 'Alice'; // 像修改可变对象一样操作
draft.todos.push({ text: 'new' }); // Immer 自动生成不可变副本
});
面试建议

回答时先说清楚推荐方案(structuredClone),再说场景化选择,最后提到 Immer 这类"避免深拷贝"的方案会是加分项。面试官想听到的是你对各方案权衡取舍的理解,而不仅仅是会手写递归。

相关链接