程序员的世界里,拷贝对象就像玩俄罗斯套娃——你以为拿到的是新娃娃,打开一看里面还装着旧娃娃的灵魂!今天,让我们深入探索JavaScript中对象的"克隆术"!
一、浅拷贝:对象界的"换皮手术"
当我们想把对象A的属性复制给对象B时,最直接的方法就是赋值:
const obj = { name: 'John', age: 30 };
const o = obj; // 这可不是拷贝!
o.age = 20;
console.log(obj.age); // 20 - 原对象被修改了!
其实造成上述代码结果的原因,是引用式的数据,是把栈对应堆数据的地址复制了给另一个变量,所以修改o,也会导致obj的修改
如果想具体了解,可以看看我这篇文章
真正的浅拷贝有三种优雅姿势:
// 姿势1:展开运算符(...) - 像披萨摊开配料
const o = { ...obj };
o.age = 20; // 原对象保持30岁
// 姿势2:Object.assign() - 像快递员送货
const o1 = {};
Object.assign(o1, obj);
o1.age = 20; // 原对象依旧30岁
// 姿势3:数组专属版(文件3)
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 展开数组
const arr3 = [].concat(arr1); // concat连接
但浅拷贝有个致命弱点——只能克隆表面一层!试试这个嵌套对象:
const nestObj = {
surface: "safe",
deep: { danger: "modify me!" }
};
const shallowCopy = { ...nestObj };
shallowCopy.deep.danger = "已修改!";
console.log(nestObj.deep.danger); // "已修改!" - 原对象被污染了!
这就像给你的手机贴膜:膜是新的,但手机还是原来那台(文件3的警示)!浅拷贝只复制了第一层属性,深层属性仍然是共享的引用。当修改shallowCopy.deep时,实际上是在修改原对象的deep属性。
为什么需要浅拷贝?
- 当对象没有嵌套结构时
- 需要快速创建对象副本
- 性能要求高的场景
二、递归:代码界的"盗梦空间"
要实现真正的深拷贝,我们需要进入"梦境层级"——递归:
let i = 1;
function recursiveDream() {
console.log(`进入第${i}层梦境`);
if (i > 3) return; // 防止无限递归成植物人
i++;
recursiveDream(); // 梦中入梦!
console.log(`从第${i-1}层梦境醒来`);
}
recursiveDream();
输出结果:
进入第1层梦境
进入第2层梦境
进入第3层梦境
进入第4层梦境
从第3层梦境醒来
从第2层梦境醒来
从第1层梦境醒来
递归就像《盗梦空间》的造梦师,每一层都创建一个新场景。这正是深拷贝需要的——为每个嵌套对象创建全新副本!
递归三要素:
- 终止条件:if (i > 3) return
- 递归调用:recursiveDream()
- 状态变化:i++
没有终止条件的递归就像无限坠落的梦境,最终导致"栈溢出"错误。这就像在梦里不停下楼梯,最后摔醒一样痛苦!
递归的妙用:
- 遍历树形结构
- 解决分治问题
- 实现深度克隆
- 解析嵌套数据
三、深拷贝:对象的"克隆人战争"
1. 手动版深拷贝
function deepCopy(newObj, oldObj) {
for (let key in oldObj) {
// 遇到数组:创建新数组并递归拷贝
if (oldObj[key] instanceof Array) {
newObj[key] = [];
deepCopy(newObj[key], oldObj[key]);
}
// 遇到对象:创建新对象并递归拷贝
else if (oldObj[key] instanceof Object) {
newObj[key] = {};
deepCopy(newObj[key], oldObj[key]);
}
// 基本类型:直接复制
else {
newObj[key] = oldObj[key];
}
}
}
// 使用示例
const obj = {
name: "Alice",
skills: ["JS", "CSS"],
address: { city: "Beijing" }
};
const clonedObj = {};
deepCopy(clonedObj, obj);
// 修改克隆对象
clonedObj.skills.push("React");
clonedObj.address.city = "Shanghai";
console.log(obj.skills); // ["JS", "CSS"] 未受影响
console.log(obj.address.city); // "Beijing" 未受影响
重要提示:判断顺序必须是 Array 在前!因为JavaScript中数组也是对象,instanceof Object同样返回true。顺序颠倒会导致数组被当作普通对象处理:
// 错误顺序示例
if (oldObj[key] instanceof Object) { // 数组也会进入这里
// 把数组当作普通对象处理
} else if (oldObj[key] instanceof Array) {
// 永远不会执行到这里
}
手动深拷贝的局限性:
- 无法处理循环引用(A引用B,B引用A)
- 无法复制特殊对象(Date, RegExp等)
- 性能较差(嵌套层级过深时)
2. JSON暴力法(文件6的奇技淫巧)
const clonedObj = JSON.parse(JSON.stringify(originalObj));
原理就像把对象拍成照片(JSON字符串),再用这张照片冲印新对象。但这种方法有三大限制:
-
丢失函数:照片拍不出动态行为
const obj = { func: () => console.log("hello") }; const clone = JSON.parse(JSON.stringify(obj)); console.log(clone.func); // undefined -
忽略特殊值:
- undefined → 消失
- Symbol → 消失
- NaN → 变成null
const obj = { a: undefined, b: Symbol(), c: NaN }; const clone = JSON.parse(JSON.stringify(obj)); console.log(clone); // { c: null } -
无法处理循环引用:
const obj = { name: "Alice" }; obj.self = obj; // 循环引用 JSON.stringify(obj); // 报错: Converting circular structure to JSON
3. 使用lodash库
专业的事交给专业工具:
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script>
const clonedObj = _.cloneDeep(originalObj);
</script>
lodash的深拷贝就像请了专业整容团队,无论对象多复杂都能完美克隆:
const complexObj = {
date: new Date(),
regex: /pattern/g,
fn: function() { return 42; },
circular: null
};
complexObj.circular = complexObj; // 循环引用
const clone = _.cloneDeep(complexObj);
console.log(clone.date instanceof Date); // true
console.log(clone.regex instanceof RegExp); // true
console.log(clone.fn()); // 42
console.log(clone.circular === clone); // true
lodash深拷贝的优势:
- 处理各种内置对象类型(Date, Map, Set等)
- 解决循环引用问题
- 保留函数实现
- 高效稳定的算法
四、拷贝方式性能 PK
| 方式 | 速度 | 功能完整性 | 使用难度 | 适用场景 |
|---|---|---|---|---|
| 赋值 | ⚡️⚡️⚡️⚡️⚡️ | ❌ | 😊 | 不需要真正拷贝时 |
| 浅拷贝 | ⚡️⚡️⚡️⚡️ | ★★☆☆☆ | 😊 | 无嵌套对象、数组 |
| 手动深拷贝 | ⚡️⚡️ | ★★★☆☆ | 😅 | 简单嵌套结构、学习目的 |
| JSON法 | ⚡️⚡️⚡️ | ★★☆☆☆ | 😊 | 无函数、特殊值的对象 |
| lodash | ⚡️⚡️⚡️ | ★★★★★ | 😄 | 生产环境、复杂对象 |
、实战选择指南
-
闪电操作需求 → 浅拷贝
// 配置对象浅拷贝 const defaultConfig = { theme: 'light', size: 'medium' }; const userConfig = { ...defaultConfig, ...userInput }; -
简单嵌套对象 → JSON法
// 保存状态到本地存储 const state = { user: { name: 'Bob' }, settings: { darkMode: true } }; localStorage.setItem('appState', JSON.stringify(state)); -
生产环境复杂对象 → lodash
// React中不可变状态更新 const newState = _.cloneDeep(prevState); newState.user.profile.avatar = "new.jpg"; setState(newState); -
面试想炫技 → 手动递归实现
// 展示对递归和原型链的理解 function deepClone(obj, cache = new WeakMap()) { // 处理循环引用... } -
想快速搞垮项目 → 直接赋值 😈
// 灾难代码示例 const config = require('./config'); const maliciousUpdate = config; maliciousUpdate.db.password = 'hacked';
结语:拷贝如人生
在JavaScript的世界里,理解深浅拷贝就像理解人际关系:
- 浅拷贝是点头之交(表面热情,内心疏离)
- 深拷贝是灵魂伴侣(完全接纳你的每一面)
- 直接赋值则是共生关系(一损俱损,一荣俱荣)
下次拷贝对象时,不妨问问自己:"我想要的是表面关系,还是灵魂共鸣?" 选择正确的拷贝方式,让你的代码关系更健康!