深拷贝与浅拷贝
问题
手写实现浅拷贝和深拷贝,要求能处理循环引用、特殊对象类型等边界情况。
答案
拷贝是将一个对象的值复制到另一个对象。浅拷贝只复制第一层属性,嵌套对象仍共享引用;深拷贝递归复制所有层级,完全独立。
浅拷贝
概念
浅拷贝只复制对象的第一层属性,如果属性值是引用类型,则复制的是引用地址。
实现方式
// 方式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()) 的对比:
| 特性 | structuredClone | JSON.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;
}
各类型处理方式汇总:
| 类型 | 处理方式 | 说明 |
|---|---|---|
Date | new Date(obj.getTime()) | 用时间戳创建新实例 |
RegExp | new RegExp(obj.source, obj.flags) | 用源码和标志创建新实例 |
Map | 遍历 + 递归拷贝键值 | 键和值都需要深拷贝 |
Set | 遍历 + 递归拷贝元素 | 每个元素都需要深拷贝 |
ArrayBuffer | obj.slice(0) | 原生方法创建副本 |
TypedArray | 基于新 buffer 创建 | Int8Array、Float32Array 等 |
Error | new Error(msg) + 复制属性 | 保留 name、message、stack |
Array | 遍历 + 递归 | 按索引逐个深拷贝 |
Object | Object.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 这类"避免深拷贝"的方案会是加分项。面试官想听到的是你对各方案权衡取舍的理解,而不仅仅是会手写递归。