今天,我学会了给对象"整容":深拷贝与浅拷贝的奇妙冒险 😄

70 阅读5分钟

程序员的世界里,拷贝对象就像玩俄罗斯套娃——你以为拿到的是新娃娃,打开一看里面还装着旧娃娃的灵魂!今天,让我们深入探索JavaScript中对象的"克隆术"!

一、浅拷贝:对象界的"换皮手术"

当我们想把对象A的属性复制给对象B时,最直接的方法就是赋值:

const obj = { name: 'John', age: 30 };
const o = obj; // 这可不是拷贝!
o.age = 20;
console.log(obj.age); // 20 - 原对象被修改了!

其实造成上述代码结果的原因,是引用式的数据,是把栈对应堆数据的地址复制了给另一个变量,所以修改o,也会导致obj的修改 如果想具体了解,可以看看我这篇文章

image.png 真正的浅拷贝有三种优雅姿势:

// 姿势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层梦境醒来

递归就像《盗梦空间》的造梦师,每一层都创建一个新场景。这正是深拷贝需要的——为每个嵌套对象创建全新副本!

递归三要素:

  1. 终止条件:if (i > 3) return
  2. 递归调用:recursiveDream()
  3. 状态变化: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字符串),再用这张照片冲印新对象。但这种方法有三大限制:

  1. 丢失函数:照片拍不出动态行为

    const obj = { func: () => console.log("hello") };
    const clone = JSON.parse(JSON.stringify(obj));
    console.log(clone.func); // undefined
    
  2. 忽略特殊值

    • undefined → 消失
    • Symbol → 消失
    • NaN → 变成null
    const obj = { a: undefined, b: Symbol(), c: NaN };
    const clone = JSON.parse(JSON.stringify(obj));
    console.log(clone); // { c: null }
    
  3. 无法处理循环引用

    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的世界里,理解深浅拷贝就像理解人际关系:

  • 浅拷贝是点头之交(表面热情,内心疏离)
  • 深拷贝是灵魂伴侣(完全接纳你的每一面)
  • 直接赋值则是共生关系(一损俱损,一荣俱荣)

下次拷贝对象时,不妨问问自己:"我想要的是表面关系,还是灵魂共鸣?" 选择正确的拷贝方式,让你的代码关系更健康!