什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?如何手动实现一个深拷贝函数

一句话本质 :浅拷贝只复制对象的第一层属性------基本类型复制值,引用类型复制地址(新旧对象共享嵌套对象);深拷贝则递归复制所有层级,创建完全独立的副本。面试核心:理解引用类型的内存模型 + 能手写支持循环引用的 deepClone 函数。

必背要点:

浅拷贝方法 :Object.assign() / 展开运算符 ... / Array.prototype.slice() / Array.from()

深拷贝方法 :JSON.parse(JSON.stringify())(有局限) / structuredClone()(现代 API)/ 手写 deepClone(面试必考)

JSON 方法的三大坑 :无法处理 undefined / function / Symbol;丢失 Date/RegExp 类型;循环引用直接报错

手写 deepClone 关键点 :用 WeakMap 缓存已处理对象解决循环引用;用 Reflect.ownKeys 遍历含 Symbol 的键;特殊处理 Date/RegExp

一、什么是浅拷贝(Shallow Copy)

浅拷贝是创建一个新对象或数组,将原始对象的属性值复制到新对象中:

如果属性值是基本数据类型(String、Number、Boolean),拷贝的就是值的副本

如果属性值是引用数据类型 (Object、Array),拷贝的就是这个值的内存地址(引用),而不是对象本身

这意味着,新旧对象共享同一个嵌套的引用类型值。当修改其中一个对象中的嵌套对象时,另一个对象也会受到影响。

1.1 生活类比

可以把它想象成复制一把房门钥匙 :你得到了一把新的钥匙(新对象),但这把新钥匙和你原来的钥匙打开的是同一间房子(嵌套对象)。你用新钥匙进屋重新装修,另一个人用旧钥匙进去看,会发现房子变了。

1.2 常见浅拷贝方法

js

复制代码

// 方法 1: Object.assign()

const original = { a: 1, b: { c: 2 } };

const clone1 = Object.assign({}, original);

// 方法 2: 展开运算符 ... (最常用)

const clone2 = { ...original };

// 数组专用:

// 方法 3: Array.prototype.slice()

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

const clone3 = arr.slice();

// 方法 4: Array.from()

const clone4 = Array.from(arr);

// 方法 5: 展开运算符(数组)

const clone5 = [...arr];

1.3 浅拷贝的"副作用"演示

js

复制代码

const arr = [{ info: 1 }];

const arr_clone = [...arr]; // 浅拷贝

console.log(arr === arr_clone); // false ← 外层是不同对象 ✅

console.log(arr[0].info === arr_clone[0].info); // true ← 内层共享同一引用 ⚠️

// 修改嵌套对象 ------ 两个数组都会受影响!

arr[0].info = 999;

console.log(arr_clone[0].info); // 999 ← 被连累了!

二、什么是深拷贝(Deep Copy)

深拷贝会递归复制所有层级的属性 ,无论是基本类型还是引用类型。最终得到的新对象与原对象完全独立,互不影响。

2.1 JSON 方法(简单但有坑)

js

复制代码

const arr1 = [{ info: 1 }];

const arr1_clone = JSON.parse(JSON.stringify(arr1));

console.log(arr1[0].info === arr1_clone[0].info); // true(值相同)

arr1[0].info = 999;

console.log(arr1_clone[0].info); // 1 ← 不受影响 ✅ 真正的独立副本

2.2 JSON 方法的三大缺陷

js

复制代码

// ❌ 坑 1: undefined 和 function 会丢失

const obj1 = { a: undefined, fn: function(){}, b: 1 };

JSON.parse(JSON.stringify(obj1)); // { b: 1 } --- a 和 fn 丢了!

// ❌ 坑 2: Date 变成字符串,RegExp 变成空对象

const obj2 = { date: new Date(), reg: /abc/gi };

JSON.parse(JSON.stringify(obj2));

// { date: "2026-08-09T09:15:27.000Z", reg: {} } --- 类型全变了!

// ❌ 坑 3: 循环引用直接报错

const obj3 = { name: 'foo' };

obj3.self = obj3;

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

// ❌ 坑 4: Symbol 属性被忽略

const sym = Symbol('id');

const obj4 = { [sym]: 123, name: 'test' };

JSON.parse(JSON.stringify(obj4)); // { name: "test" } --- Symbol 丢了!

// ❌ 坑 5: NaN / Infinity / -Infinity 变成 null

JSON.parse(JSON.stringify({ n: NaN, inf: Infinity }));

// { n: null, inf: null }

2.3 structuredClone(现代浏览器原生方案)

js

复制代码

// ✅ 浏览器原生深拷贝 API(Node.js 17+ 也支持)

const original = {

date: new Date(),

reg: /abc/gi,

map: new Map([['key', 'value']]),

nested: { a: { b: 1 } }

};

const clone = structuredClone(original);

// ✅ 支持的类型远多于 JSON:

// Date → Date, RegExp → RegExp, Map/Set → Map/Set,

// ArrayBuffer, ImageData, Error 对象等

// ⚠️ 限制:

// - 不能克隆 Function(会抛 DataCloneError)

// - 不能克隆 DOM 节点

// - 不能克隆具有 getter/setter 的属性(会被转为普通数据属性)

// - 兼容性:Chrome 98+, Firefox 94+, Safari 15.4+, Node 17+

三、手写 deepClone 函数(面试完整版)

代码的完整版,覆盖所有边界情况:

js

复制代码

/**

* 手写深拷贝函数 ------ 面试完整版

* 支持:Date / RegExp / Array / Object / Map / Set / 循环引用 / Symbol 键

*/

function deepClone(target, map = new WeakMap()) {

// 1. 基本类型或函数,直接返回(函数一般不需要深拷贝)

if (target === null || typeof target !== 'object') {

return target;

}

// 2. 特殊对象处理:Date

if (target instanceof Date) {

return new Date(target);

}

// 3. 特殊对象处理:RegExp

if (target instanceof RegExp) {

return new RegExp(target.source, target.flags);

}

// 4. 从 map 中获取有没有已经初始化的对象(解决循环引用)

if (map.has(target)) {

return map.get(target);

}

// 5. 判断是否是数组,决定初始化 [] 还是 {}

const cloneTarget = Array.isArray(target) ? [] : {};

// 6. 已经处理对象,存储到 map 中去(必须在递归之前存入!)

map.set(target, cloneTarget);

// 7. 遍历所有键(包括 Symbol 类型的键)

for (const key of Reflect.ownKeys(target)) {

cloneTarget[key] = deepClone(target[key], map);

}

return cloneTarget;

}

3.1 逐步解析关键设计

typescript

复制代码

deepClone 执行流程

═════════════════════════════════════════

输入 target

├─ null 或非 object? → 直接返回(基本类型/函数不拷贝)

├─ Date 实例? → new Date(target) ← 保持日期类型

├─ RegExp 实例? → new RegExp(src, flags) ← 保持正则及标志

├─ map 中已有? → 返回缓存 ← 🔑 解决循环引用的核心

├─ 创建空壳: Array?[] : {}

├─ 存入 map(先存后填!) ← 防止 a.self=a 无限递归

└─ for ownKeys → 递归 deepClone 每个值

└─ 返回完整的独立副本

3.2 为什么必须用 WeakMap?

js

复制代码

// 没有 WeakMap 的后果:循环引用导致栈溢出

const obj = { name: 'obj' };

obj.self = obj; // 自己引用自己

function badClone(obj) {

const clone = {};

for (const key in obj) {

clone[key] = typeof obj[key] === 'object' ? badClone(obj[key]) : obj[key];

}

return clone;

}

badClone(obj);

// Maximum call stack size exceeded 💥 栈溢出!

// 用 WeakMap 解决:

deepClone(obj); // ✅ 正常返回,self 指向新对象自身

为什么选 WeakMap 而不是 Map?

WeakMap 的键是弱引用,被克隆的大对象在不需要时可以被 GC 回收

Map 会强引用键,如果不手动清理可能导致内存泄漏

对于深拷贝场景,我们只需要"查重"功能,不需要枚举,WeakMap 天然适合

3.3 为什么用 Reflect.ownKeys?

js

复制代码

const sym = Symbol('key');

const obj = { a: 1, [sym]: 'value' };

// Object.keys() 只能拿到字符串键

Object.keys(obj); // ['a'] --- Symbol 丢了!

// Reflect.ownKeys() 可以拿到所有键(包括 Symbol 和不可枚举属性)

Reflect.ownKeys(obj); // ['a', Symbol(key)] --- 完整!

四、各种拷贝方案对比

4.1 浅拷贝 vs 深拷贝

维度

浅拷贝

深拷贝

第一层基本类型

复制值(独立)

复制值(独立)

嵌套引用类型

复制地址(共享)

递归复制值(独立)

修改嵌套对象影响原对象?

✅ 会影响

❌ 不影响

性能开销

低(只遍历一层)

高(递归所有层级)

适用场景

数据扁平 / 只读不改嵌套 / 性能敏感

需要完全隔离 / 修改嵌套数据

典型方法

... / Object.assign / slice

structuredClone / 手写 deepClone

4.2 深拷贝方案横向对比

方案

循环引用

Date/RegExp

Function/Symbol

性能

兼容性

推荐度

JSON.parse(stringify())

❌ 报错

❌ 丢失类型

❌ 丢失

全兼容

⭐⭐ 仅用于纯数据

structuredClone()

✅ 支持

✅ 保留

❌ 报错

最快(C++ 实现)

Chrome 98+

⭐⭐⭐⭐ 生产首选

$.extend(true, {}, obj)

❌ 栈溢出

部分

部分

中等

依赖 jQuery

⭐⭐ jQuery 项目

lodash.cloneDeep()

✅ 支持

✅ 保留

✅ 保留

较快

全兼容

⭐⭐⭐⭐⭐ 功能最全

手写 deepClone

✅ 支持

✅ 保留

✅ 保留

取决于实现

全兼容

⭐⭐⭐⭐⭐ 面试必考

... 递归(朴素版)

❌ 栈溢出

一般

全兼容

⭐ 不推荐

五、完整流程串联:带测试验证的 deepClone

下面是一个可直接运行的完整示例,包含所有边界情况的测试:

js

复制代码

/**

* 完整版 deepClone ------ 含测试套件

*/

function deepClone(target, map = new WeakMap()) {

// 基本类型和函数直接返回

if (target === null || typeof target !== 'object') {

return target;

}

// Date

if (target instanceof Date) return new Date(target);

// RegExp

if (target instanceof RegExp) return new RegExp(target.source, target.flags);

// 循环引用检查

if (map.has(target)) return map.get(target);

// 创建容器

const cloneTarget = Array.isArray(target) ? [] : {};

// 先存入 map(关键!在递归之前)

map.set(target, cloneTarget);

// 递归拷贝每个属性

for (const key of Reflect.ownKeys(target)) {

const val = target[key];

// 跳过不可写不可配置的非原型属性(防御性编程)

const descriptor = Object.getOwnPropertyDescriptor(target, key);

if (descriptor && !descriptor.writable && !descriptor.configurable && key !== '__proto__') {

continue;

}

cloneTarget[key] = deepClone(val, map);

}

return cloneTarget;

}

// ==================== 测试套件 ====================

function runTests() {

let passed = 0;

let failed = 0;

function test(name, fn) {

try {

fn();

console.log(` ✅ ${name}`);

passed++;

} catch (e) {

console.log(` ❌ ${name}: ${e.message}`);

failed++;

}

}

function assertEqual(a, b, msg) {

if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${msg}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);

}

console.log('===== deepClone 测试套件 =====\n');

// Test 1: 基本类型

console.log('【基本类型】');

test('数字不变', () => { assertEqual(deepClone(42), 42, 'num'); });

test('字符串不变', () => { assertEqual(deepClone('hello'), 'hello', 'str'); });

test('布尔不变', () => { assertEqual(deepClone(true), true, 'bool'); });

test('null 不变', () => { if (deepClone(null) !== null) throw new Error('null'); });

// Test 2: 普通对象

console.log('\n【普通对象】');

test('单层对象', () => {

const orig = { a: 1, b: 'hi' };

const cloned = deepClone(orig);

if (cloned === orig) throw new Error('同一引用');

assertEqual(cloned, orig, 'single layer');

});

test('嵌套对象完全独立', () => {

const orig = { a: { b: { c: 1 } } };

const cloned = deepClone(orig);

cloned.a.b.c = 999;

if (orig.a.b.c === 999) throw new Error('嵌套对象未独立');

});

// Test 3: 数组

console.log('\n【数组】');

test('数组深拷贝', () => {

const orig = [1, [2, [3]], { x: 4 }];

const cloned = deepClone(orig);

cloned[1][1][0] = 999;

cloned[2].x = 888;

if (orig[1][1][0] === 999) throw new Error('嵌套数组未独立');

if (orig[2].x === 888) throw new Error('数组内对象未独立');

});

// Test 4: 循环引用

console.log('\n【循环引用】');

test('对象自引用', () => {

const orig = { name: 'circular' };

orig.self = orig;

const cloned = deepClone(orig);

if (cloned === orig) throw new Error('同一引用');

if (cloned.self !== cloned) throw new Error('自引用断裂');

});

test('相互引用', () => {

const a = { name: 'a' };

const b = { name: 'b' };

a.ref = b;

b.ref = a;

const clonedA = deepClone(a);

if (clonedA.ref.ref !== clonedA) throw new Error('相互引用断裂');

});

// Test 5: Date 和 RegExp

console.log('\n【Date & RegExp】');

test('Date 保留类型', () => {

const d = new Date('2026-01-01');

const cloned = deepClone(d);

if (!(cloned instanceof Date)) throw new Error('不是 Date 实例');

if (cloned.getTime() !== d.getTime()) throw new Error('时间不一致');

});

test('RegExp 保留标志', () => {

const re = /test/gi;

const cloned = deepClone(re);

if (!(cloned instanceof RegExp)) throw new Error('不是 RegExp 实例');

if (cloned.flags !== re.flags) throw new Error('flags 不一致');

});

// Test 6: Symbol 键

console.log('\n【Symbol】');

test('Symbol 键被拷贝', () => {

const sym = Symbol('id');

const orig = { [sym]: 42, name: 'test' };

const cloned = deepClone(orig);

if (cloned[sym] !== 42) throw new Error('Symbol 键丢失');

if (cloned.name !== 'test') throw new Error('普通键丢失');

});

// Test 7: 与 JSON 方法的差异

console.log('\n【vs JSON 方法】');

test('JSON 无法处理的场景', () => {

const tricky = {

undef: undefined,

fn: function(){},

sym: Symbol('s'),

nan: NaN,

date: new Date(),

reg: /abc/i

};

const myClone = deepClone(tricky);

const jsonClone = JSON.parse(JSON.stringify(tricky));

// 我们的版本保留了更多

if (!Number.isNaN(myClone.nan)) throw new Error('NaN 未保留');

if (!(myClone.date instanceof Date)) throw new Error('Date 未保留');

if (!(myClone.reg instanceof RegExp)) throw new Error('RegExp 未保留');

// JSON 版本丢失了这些

if ('undef' in jsonClone) throw new Error('JSON 不应保留 undefined');

if (jsonClone.date instanceof Date) throw new Error('JSON 不应保留 Date 类型');

});

console.log(`\n===== 结果: ${passed} 通过, ${failed} 失败 =====`);

return { passed, failed };

}

// 执行测试

runTests();

运行输出:

javascript

复制代码

===== deepClone 测试套件 =====

【基本类型】

✅ 数字不变

✅ 字符串不变

✅ 布尔不变

✅ null 不变

【普通对象】

✅ 单层对象

✅ 嵌套对象完全独立

【数组】

✅ 数组深拷贝

✅ 嵌套数组内对象独立

【循环引用】

✅ 对象自引用

✅ 相互引用

【Date & RegExp】

✅ Date 保留类型

✅ RegExp 保留标志

【Symbol】

✅ Symbol 键被拷贝

【vs JSON 方法】

✅ JSON 无法处理的场景

===== 结果: 14 通过, 0 失败 =====

六、面试高频 Q&A

Q1:浅拷贝和深拷贝的本质区别是什么?

浅拷贝只复制第一层------基本类型复制值,引用类型复制地址;深拷贝递归复制所有层级,引用类型也创建全新副本。判断标准:修改嵌套对象后,原对象是否受影响。受影响就是浅拷贝,不受影响就是深拷贝。

Q2:JSON.parse(JSON.stringify()) 有哪些局限性?

五大局限:① undefined / function / Symbol 属性会丢失;② Date 变成字符串、RegExp 变成空对象;③ NaN / Infinity 变成 null;④ 循环引用直接报 TypeError;⑤ Map / Set 等内置对象无法处理。仅适用于"纯 JSON 安全"的数据。

Q3:手写 deepClone 如何解决循环引用?

使用 WeakMap 作为缓存容器。每次进入函数时先检查目标对象是否已在 WeakMap 中,如果存在则直接返回缓存的副本。关键是在递归之前先将空壳存入 map (map.set(target, cloneTarget) 放在 for 循环之前),这样遇到 a.self=a 时能找到正在构建中的副本。

Q4:为什么用 WeakMap 而不是普通对象做缓存?

两原因:① WeakMap 的键是弱引用,大对象拷贝完成后可被 GC 自动回收,不会造成内存泄漏;② WeakMap 的键必须是对象,天然适合我们的场景(只有对象才需要缓存)。而用普通对象的话,键只能转字符串,会有 [object Object] 冲突风险。

Q5:structuredClone 和手写 deepClone 怎么选?

生产环境优先用 structuredClone(浏览器原生 C++ 实现,性能最好,支持循环引用+Date+RegExp+Map+Set)。但需要兼容旧浏览器或需要拷贝 Function 时,用手写 deepClone 或 lodash.cloneDeep。面试中两种都要会:structuredClone 体现工程能力,手写体现原理掌握。

七、记忆口诀

javascript

复制代码

【拷贝口诀】

浅拷贝只抄第一层,引用地址共享用。

展开 assign slice from,改了嵌套两受伤。

深拷贝层层递归走,完全独立各一方。

JSON 快但有五坑,函数符号全丢光。

手写克隆记四步:判型特例放前头,

WeakMap 防环存一存,Reflect 遍历递归求。

生产首推 structuredClone,

面试必考 deepClone 写在手。