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