引言
在 JavaScript 开发中,对象的复制是一个看似简单却极易出错的问题。很多初学者甚至中级开发者都曾因"浅拷贝"导致数据意外修改而陷入调试困境。本文将从底层内存机制出发,系统讲解 栈内存与堆内存的区别 、它们如何协同工作 ,并深入剖析 什么是真正的深拷贝,以及如何在实际开发中安全地实现它。
一、栈内存 vs 堆内存:程序运行的"骨架"与"血肉"
要真正理解深拷贝,必须先了解 JavaScript(以及其他高级语言)是如何管理内存的。程序运行时,内存主要分为两个区域:栈(Stack) 和 堆(Heap) 。
1. 栈内存:自动管理的"储物架"
-
特点:
- 遵循 先进后出(LIFO, Last In First Out) 原则。
- 存储 函数调用上下文 、局部变量 和 基本数据类型 (如
number、string、boolean、undefined、null、symbol、bigint)。 - 内存分配和释放由系统 自动完成,效率极高。
-
生命周期:
- 当一个函数被调用时,其局部变量会被压入栈中;
- 函数执行完毕后,整个栈帧被弹出,内存自动释放。
✅ 举例:
csharpfunction foo() { let a = 10; // a 存在栈中 let b = "hello"; // b 也存在栈中 } foo(); // 执行结束后,a 和 b 自动销毁
2. 堆内存:手动(或半自动)管理的"大仓库"
-
特点:
- 用于存储 复杂数据结构,如对象(Object)、数组(Array)、函数(Function)等。
- 内存分配灵活,但访问速度略慢于栈。
- 在 JavaScript 中,堆内存由 垃圾回收机制(GC) 管理,而非程序员手动释放。
-
生命周期:
- 对象一旦创建,就存在于堆中;
- 只有当没有任何引用指向该对象时,垃圾回收器才会将其回收。
✅ 举例:
csharplet user = { name: "Alice", age: 25 }; // 对象 { name: "Alice", age: 25 } 存在于堆中 // 变量 user(在栈中)保存的是该对象的内存地址(引用)
二、栈与堆的协作:引用机制揭秘
JavaScript 中的对象操作本质上是 通过引用进行的。这种设计极大提升了性能,但也带来了"共享副作用"的风险。
关键关系:
- 栈存引用,堆存实体
当你声明一个对象变量时,变量本身(引用)存储在栈中 ,而对象的实际内容存储在堆中。 - 赋值即复制引用
如果你将一个对象赋值给另一个变量,实际上只是复制了栈中的引用地址,两个变量指向同一个堆内存位置。
❗ 危险示例(浅拷贝陷阱):
iniconst users = [ { id: 1, name: '张三' ,hometown:'北京'}, { id: 2, name: '李四' ,hometown:'上海'}, { id: 3, name: '王五' ,hometown:'广州'} ]; const data = users; data.hobbies = ['篮球','足球','跑步']; console.log(data, users);
此时运行代码结果如下:
data内容的修改同时发生在users和data上

- 生命周期解耦
栈中变量的销毁(如函数结束)不会立即删除堆中的对象,只有当所有引用都消失后,对象才会被 GC 回收。
三、什么是深拷贝?为什么需要它?
定义
深拷贝(Deep Copy) 是指:递归地复制对象及其所有嵌套属性,在堆内存中创建一个全新的、完全独立的对象副本。新对象与原对象没有任何引用关联。
目标
- 修改副本 不影响原对象;
- 副本拥有 完整的数据结构副本,包括嵌套对象、数组等。
四、实现深拷贝的常用方法
方法 1:JSON 序列化 + 反序列化(最简单但有限制)
这是前端开发中最常用的"伪深拷贝"技巧:
less
var users;
var data;
users = [ { id: 1, name: '张三' ,hometown:'北京'}, { id: 2, name: '李四' ,hometown:'上海'}, { id: 3, name: '王五' ,hometown:'广州'}];
// 深拷贝,是指在堆内存中,重新分配一个内存空间,存储拷贝的对象,而不是引用地址。
// 序列化 :把对象转换为字符串 JSON.stringify()
// 反序列化 :把字符串转换为对象 JSON.parse()
var data = JSON.parse(JSON.stringify(users));
data[0]['hobbies'] = ['篮球','足球','跑步'];
console.log(data, users);
运行结果:

✅ 优点:
- 代码简洁,一行搞定;
- 对纯 JSON 兼容的数据结构非常有效。
❌ 致命缺陷:
| 问题 | 说明 |
|---|---|
| 函数丢失 | function 会被忽略(JSON.stringify 不处理函数) |
| undefined 丢失 | 属性值为 undefined 的字段会被删除 |
| Symbol 键丢失 | Symbol 作为 key 无法被序列化 |
| 循环引用崩溃 | 对象自引用会导致 JSON.stringify 报错 |
| Date 变字符串 | new Date() 会被转为 ISO 字符串,不再是 Date 对象 |
| RegExp、Error 等特殊对象失效 | 转为普通对象或空对象 |
方法 2:手写递归深拷贝(更健壮)
为了克服 JSON 方法的局限,我们可以手动实现一个支持更多类型的深拷贝函数:
javascript
// 手写递归深拷贝
users = [
{ id: 1, name: '张三' ,hometown:'北京'},
{ id: 2, name: '李四' ,hometown:'上海'},
{ id: 3, name: '王五' ,hometown:'广州'}
];
function deepClone(obj, hash = new WeakMap()) {
// 处理 null 和非对象类型
if (obj === null || typeof obj !== "object") return obj;
// 防止循环引用
if (hash.has(obj)) return hash.get(obj);
// 处理 Date
if (obj instanceof Date) return new Date(obj);
// 处理 RegExp
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
// 处理 Array 和 Object
const cloned = Array.isArray(obj) ? [] : {};
// 记录引用,防止循环
hash.set(obj, cloned);
// 递归拷贝所有属性(包括 Symbol)
Reflect.ownKeys(obj).forEach(key => {
cloned[key] = deepClone(obj[key], hash);
});
return cloned;
}
const data = deepClone(users);
data[0]['hobbies'] = ['篮球','足球','跑步'];
console.log(data, users);
运行结果:

✅ 优势:
- 支持
Date、RegExp、Symbol键; - 能处理循环引用(通过
WeakMap缓存已拷贝对象); - 保留函数(若需要可扩展);
- 更接近"真正"的深拷贝。
💡 提示:生产环境中建议使用成熟库(如 Lodash 的
_.cloneDeep),避免重复造轮子。
方法 3:使用第三方库(推荐生产环境)
- Lodash :
_.cloneDeep(value) - jQuery :
$.extend(true, {}, obj)(已不推荐) - structuredClone() (现代浏览器原生支持)
新标准:structuredClone()
ES2022 引入了全局函数 structuredClone(),专为深拷贝设计:
ini
const copy = structuredClone(original);
支持:
- 循环引用
Date、RegExp、Map、Set、ArrayBuffer等undefined、Symbol(部分限制)
注意:
- 仍不支持函数、DOM 节点等;
- 需要较新浏览器(Chrome 98+,Node.js 17+)
五、总结:何时用哪种拷贝?
| 场景 | 推荐方法 |
|---|---|
| 简单对象,无函数/日期/循环引用 | JSON.parse(JSON.stringify(obj)) |
| 需要兼容旧环境,且结构复杂 | 手写递归 or Lodash _.cloneDeep |
| 现代项目,追求标准与性能 | structuredClone() |
| 仅需第一层拷贝(浅拷贝) | {...obj} 或 Object.assign({}, obj) |
六、结语
深拷贝不仅是语法技巧,更是对 内存模型 和 数据所有权 的深刻理解。掌握栈与堆的协作机制,能帮助我们写出更安全、更高效的代码。在实际开发中,请根据数据结构的复杂度和运行环境,选择最合适的拷贝策略。
记住 :
浅拷贝是"共用一本日记",
深拷贝是"誊抄一本新日记"。
别让别人的涂改,毁了你的原始记录!