深拷贝、浅拷贝与堆栈面试题复盘
这是前端面试里非常常见的一类问题:
什么是深拷贝和浅拷贝?它们和堆、栈有什么关系?
这道题看起来像是在问 API,实际上是在考察你是否理解 JavaScript 的数据类型、内存模型和引用关系。
如果只回答“浅拷贝只拷贝一层,深拷贝会递归拷贝”,只能算答到表面。更完整的回答应该从基本类型和引用类型讲起,再解释为什么对象拷贝容易互相影响。
在 JavaScript 中,数据大致可以分成两类:
- 基本类型:
string、number、boolean、undefined、null、symbol、bigint - 引用类型:
object、array、function、Date、Map、Set等
基本类型保存的是值本身,赋值时通常是值的复制。
引用类型保存的是对象的引用,赋值时复制的是引用地址,所以多个变量可能指向同一个对象。
浅拷贝只拷贝对象第一层。如果第一层里还有对象,里面的对象仍然共享引用。
深拷贝会把嵌套对象也复制出来,让新对象和旧对象尽量互不影响。
栈和堆怎么理解
Section titled “栈和堆怎么理解”面试里常见说法是:
- 基本类型的值通常放在栈中。
- 引用类型的对象内容通常放在堆中。
- 变量里保存的是指向堆中对象的引用。
这是一种帮助理解的简化模型,不需要把它讲得像浏览器引擎源码一样复杂。
可以这样理解:
let name = 'xiaoxi';
栈:name -> 'xiaoxi'基本类型比较直接,变量和值之间的关系很简单。
再看对象:
const user = { name: 'xiaoxi', profile: { age: 18, },};可以理解为:
栈:user -> 引用地址 0x001
堆:0x001 -> { name: 'xiaoxi', profile: 引用地址 0x002}
0x002 -> { age: 18}对象本身放在堆里,变量 user 保存的是引用。
赋值不是拷贝对象
Section titled “赋值不是拷贝对象”很多拷贝问题都从赋值开始。
const user1 = { name: 'xiaoxi', profile: { age: 18, },};
const user2 = user1;
user2.name = 'veyliss';
console.log(user1.name); // veyliss这里 user2 = user1 并没有创建一个新对象,只是让 user2 和 user1 指向同一个对象。
所以修改 user2.name,user1.name 也会变。
可以理解为:
user1 -> 0x001user2 -> 0x001两个变量指向同一个堆对象。
什么是浅拷贝
Section titled “什么是浅拷贝”浅拷贝会创建一个新的外层对象,但里面的引用类型属性仍然和原对象共享。
常见浅拷贝方式:
- 展开运算符:
{ ...obj } Object.assign()- 数组的
slice()、concat()、Array.from()、[...arr]
看一个例子:
const user1 = { name: 'xiaoxi', profile: { age: 18, },};
const user2 = { ...user1,};
user2.name = 'veyliss';user2.profile.age = 20;
console.log(user1.name); // xiaoxiconsole.log(user1.profile.age); // 20为什么 name 没有互相影响,但 profile.age 互相影响了?
因为 user2 是一个新的外层对象,但 profile 仍然指向同一个内部对象。
可以理解为:
user1 -> 0x001 -> { name: 'xiaoxi', profile: 0x002}
user2 -> 0x003 -> { name: 'xiaoxi', profile: 0x002}外层对象不同,内层 profile 相同。
这就是浅拷贝。
什么是深拷贝
Section titled “什么是深拷贝”深拷贝会递归复制对象中的嵌套对象,让新对象和旧对象不再共享内部引用。
const user1 = { name: 'xiaoxi', profile: { age: 18, },};
const user2 = structuredClone(user1);
user2.profile.age = 20;
console.log(user1.profile.age); // 18console.log(user2.profile.age); // 20这时可以理解为:
user1 -> 0x001 -> profile -> 0x002user2 -> 0x003 -> profile -> 0x004外层对象不同,内层对象也不同。
JSON 方法的深拷贝
Section titled “JSON 方法的深拷贝”以前很常见的一种写法是:
const copy = JSON.parse(JSON.stringify(source));这种方式简单,但有明显限制。
它适合普通 JSON 数据:
const source = { name: 'xiaoxi', tags: ['前端', 'JavaScript'],};
const copy = JSON.parse(JSON.stringify(source));但它处理不了很多特殊值:
const source = { name: 'xiaoxi', createdAt: new Date(), sayHello() { console.log('hello'); }, value: undefined,};
const copy = JSON.parse(JSON.stringify(source));
console.log(copy.createdAt); // 字符串,不再是 Dateconsole.log(copy.sayHello); // undefinedconsole.log(copy.value); // undefined它的主要问题包括:
Date会变成字符串。undefined、函数、symbol会丢失。Map、Set不能按原结构保留。- 遇到循环引用会直接报错。
所以面试时不要把 JSON.parse(JSON.stringify()) 说成万能深拷贝。
structuredClone
Section titled “structuredClone”现代浏览器和 Node.js 中可以使用 structuredClone()。
const source = { name: 'xiaoxi', createdAt: new Date(), items: new Map([['count', 1]]),};
const copy = structuredClone(source);
console.log(copy.createdAt instanceof Date); // trueconsole.log(copy.items instanceof Map); // true相比 JSON 方法,structuredClone() 能保留更多内置类型,也能处理循环引用。
const source = { name: 'xiaoxi',};
source.self = source;
const copy = structuredClone(source);
console.log(copy.self === copy); // true不过它也不是没有限制。函数、DOM 节点这类内容不能直接被结构化克隆。
面试里有时会要求手写一个简单版本。
最基础的写法是递归:
function deepClone(value) { if (value === null || typeof value !== 'object') { return value; }
const result = Array.isArray(value) ? [] : {};
for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { result[key] = deepClone(value[key]); } }
return result;}这个版本可以处理普通对象和数组,但不能处理循环引用,也不能完整处理 Date、Map、Set 等类型。
如果要支持循环引用,可以用 WeakMap 记录已经拷贝过的对象。
function deepClone(value, cache = new WeakMap()) { if (value === null || typeof value !== 'object') { return value; }
if (cache.has(value)) { return cache.get(value); }
if (value instanceof Date) { return new Date(value.getTime()); }
if (value instanceof Map) { const result = new Map(); cache.set(value, result);
value.forEach((item, key) => { result.set(deepClone(key, cache), deepClone(item, cache)); });
return result; }
if (value instanceof Set) { const result = new Set(); cache.set(value, result);
value.forEach((item) => { result.add(deepClone(item, cache)); });
return result; }
const result = Array.isArray(value) ? [] : {}; cache.set(value, result);
Reflect.ownKeys(value).forEach((key) => { result[key] = deepClone(value[key], cache); });
return result;}这个版本已经能覆盖不少面试场景:
- 基本类型直接返回。
- 普通对象和数组递归复制。
Date单独处理。Map、Set单独处理。WeakMap解决循环引用。Reflect.ownKeys()可以拿到symbolkey。
但它仍然不是完整工业级实现。比如属性描述符、原型链、不可枚举属性、函数、DOM 节点等,还需要更多额外处理。
浅拷贝和深拷贝怎么选
Section titled “浅拷贝和深拷贝怎么选”不是所有场景都需要深拷贝。
| 场景 | 推荐方式 |
|---|---|
| 只改第一层字段 | 浅拷贝即可 |
| React 更新一层状态 | 展开运算符或 Object.assign() |
| 嵌套对象也要完全隔离 | 深拷贝 |
| 普通 JSON 数据复制 | JSON 方法可以考虑 |
| 复杂对象、循环引用 | structuredClone() 或专门工具 |
| 高性能、大数据量场景 | 尽量避免无脑深拷贝 |
在 React 里也经常会遇到这个问题。
比如更新用户名称:
setUser((user) => ({ ...user, name: 'veyliss',}));这是浅拷贝,足够更新第一层。
如果要更新嵌套字段:
setUser((user) => ({ ...user, profile: { ...user.profile, age: 20, },}));这不是对整个对象做深拷贝,而是只拷贝发生变化的路径。这个方式在 React 里更常见,也更可控。
面试时怎么回答
Section titled “面试时怎么回答”这道题可以这样回答:
JavaScript 里基本类型通常保存值本身,引用类型保存的是对象引用。对象内容可以理解为存在堆中,变量里保存的是引用地址。普通赋值只是复制引用,不会创建新对象。浅拷贝会创建一个新的外层对象,但嵌套对象仍然共享引用;深拷贝会递归复制嵌套对象,让新旧对象尽量不共享引用。常见浅拷贝方式有展开运算符、
Object.assign()、数组slice()等;常见深拷贝方式有structuredClone()、JSON 序列化和手写递归。JSON 方法有局限,会丢失函数、undefined,也不能处理循环引用。手写深拷贝时要考虑递归、特殊类型和循环引用,可以用WeakMap做缓存。
如果面试官继续追问,可以展开这些点:
- 赋值、浅拷贝、深拷贝的区别。
- 为什么浅拷贝会影响嵌套对象。
JSON.parse(JSON.stringify())有哪些问题。structuredClone()能解决什么,不能解决什么。- 手写深拷贝如何处理循环引用。
- React 中为什么经常只拷贝变化路径。
深拷贝、浅拷贝和堆栈这道题,本质是在问你是否理解引用。
可以记住这几句话:
- 基本类型更像“直接保存值”。
- 引用类型更像“变量保存地址,对象放在堆里”。
- 赋值复制的是引用。
- 浅拷贝复制第一层。
- 深拷贝递归复制嵌套层。
- 深拷贝不是越多越好,要看业务是否真的需要完全隔离。
面试里把这些关系讲清楚,比单纯背一个手写深拷贝函数更重要。