📋 JavaScript 浅拷贝:只复制"第一层"的艺术
🤔 什么是浅拷贝?
定义 :
浅拷贝是指创建一个新对象,这个新对象拥有原对象属性值的精确拷贝。
- 如果属性是基本类型 (String, Number, Boolean...),拷贝的是值。
- 如果属性是引用类型 (Object, Array...),拷贝的是内存地址(指针)。
通俗比喻 :
想象你有一本相册(原对象)。
- 浅拷贝 :你复印了相册的封面和目录。对于照片本身,你没有重新打印,而是把原相册里的照片拿出来,放进了新相册。
- 结果:如果你在新相册里撕掉一张照片(修改引用类型的内部属性),原相册里的那张照片也坏了,因为它们其实是同一张物理照片。
- 但是,如果你给新相册换了一个封面(修改第一层基本属性),原相册不受影响。
📂 目录
- [🖼️ 图解:浅拷贝 vs 深拷贝](#🖼️ 图解:浅拷贝 vs 深拷贝)
- [🛠️ 实现浅拷贝的 4 种主流方法](#🛠️ 实现浅拷贝的 4 种主流方法)
- [⚠️ 浅拷贝的陷阱:什么时候会出问题?](#⚠️ 浅拷贝的陷阱:什么时候会出问题?)
- [💡 总结与选型建议](#💡 总结与选型建议)
1. 🖼️ 图解:浅拷贝 vs 深拷贝
假设我们有如下数据结构:
javascript
const original = {
name: "Lingma", // 基本类型
info: {
// 引用类型
age: 1,
},
};
✅ 浅拷贝后内存结构
text
Stack (栈)
+----------+ Heap (堆)
| original |-----------> +------------------+
+----------+ | name: "Lingma" |
| info: [Addr A] --+--> +------------------+
+----------+ +------------------+ | age: 1 |
| copy |-----------> +------------------+ +------------------+
+----------+ | name: "Lingma" |
| info: [Addr A] --+----^ (指向同一个对象!)
+------------------+
结论 :
original.info和copy.info指向堆中的同一个对象。
❌ 深拷贝后内存结构(对比参考)
text
Stack (栈)
+----------+ Heap (堆)
| original |-----------> +------------------+
+----------+ | name: "Lingma" |
| info: [Addr A] --+--> +------------------+
+----------+ +------------------+ | age: 1 |
| copy |-----------> +------------------+ +------------------+
+----------+ | name: "Lingma" |
| info: [Addr B] --+--> +------------------+
+------------------+ | age: 1 |
+------------------+
结论 :
copy拥有完全独立的内存空间。
2. 🛠️ 实现浅拷贝的 4 种主流方法
方法一:Object.assign() (经典常用)
这是 ES6 之前最常用的浅拷贝方法。它将源对象的所有可枚举属性复制到目标对象。
javascript
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
// 测试基本类型
copy.a = 99;
console.log(original.a); // 1 ✅ 互不影响
// 测试引用类型
copy.b.c = 99;
console.log(original.b.c); // 99 😱 原数据被修改!
注意 :第一个参数
{}是目标对象,必须传空对象,否则original自身会被修改。
方法二:展开运算符 ... (现代首选)
ES6 引入的语法糖,代码更简洁,可读性更高。
javascript
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
// 效果同 Object.assign
copy.b.c = 99;
console.log(original.b.c); // 99 😱
对于数组的浅拷贝:
javascript
const arr = [1, 2, [3, 4]];
const newArr = [...arr];
// 或者
const newArr2 = arr.slice();
// 或者
const newArr3 = Array.from(arr);
方法三:Array.prototype.slice() / concat() (数组专用)
在 ES6 之前,这是数组浅拷贝的标准做法。
javascript
const arr = [1, 2, 3];
const copy1 = arr.slice();
const copy2 = [].concat(arr);
方法四:手写浅拷贝函数(面试/定制需求)
如果需要兼容旧环境或处理特殊逻辑,可以手写。
javascript
function shallowClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
// 判断是数组还是对象
const cloneObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = obj[key]; // 直接赋值,引用类型拷贝地址
}
}
return cloneObj;
}
3. ⚠️ 浅拷贝的陷阱:什么时候会出问题?
浅拷贝最大的风险在于嵌套对象。
❌ 事故现场
javascript
const user = {
name: "Lingma",
settings: {
theme: "dark",
notifications: true,
},
};
// 浅拷贝
const newUser = { ...user };
// 用户修改设置
newUser.settings.theme = "light";
console.log(user.settings.theme); // "light" 😱
// 糟糕!原用户的设置也被改了!
✅ 如何避免?
- 如果只有一层嵌套:使用浅拷贝完全没问题,性能最好。
- 如果有多层嵌套 :
- 使用深拷贝 (
structuredClone,JSON.parse/stringify, LodashcloneDeep)。 - 或者使用不可变数据库(如 Immer, Immutable.js),它们通过结构共享实现了高效的"看似可变,实则不可变"。
- 使用深拷贝 (
4. 💡 总结与选型建议
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
{ ...obj } |
对象浅拷贝 | 语法简洁,现代标准 | 仅浅拷贝,兼容性需 babel |
Object.assign |
对象浅拷贝/合并 | 兼容性好,可合并多个对象 | 语法稍啰嗦 |
[...arr] |
数组浅拷贝 | 简洁,速度快 | 仅适用于数组/可迭代对象 |
arr.slice() |
数组浅拷贝 | 经典写法,兼容 IE | 语义不如展开运算符清晰 |
🚀 博主寄语 :
浅拷贝不是"低级"的拷贝,而是高性能 的拷贝。
在 React/Vue 的状态管理中,如果状态层级不深,浅拷贝配合不可变思维(Immutability)是最佳实践。
记住原则:
- 一层用浅,多层用深。
- 数组展开最香,对象合并 assign。
- 引用类型要小心,修改之前想清楚。
希望这篇文档能帮你彻底掌握浅拷贝!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️