从内存模型看深浅拷贝

💡 前言:为什么会有深浅拷贝的纠结?

JS 将数据分为简单数据类型(原始值)复杂数据类型(引用值)

  • 简单类型:直接存放在栈(Stack)中,赋值是"值"的完全复制,不存在深浅拷贝的概念。
  • 复杂类型 :具体内容存放在堆(Heap) 中, 中只存了一个指向堆内存的地址指针

正因为复杂数据类型的这种"地址引用"特性,当我们试图复制一个对象或数组时,普通的赋值操作(如 let obj2 = obj1)仅仅是复制了栈内存中的指针,导致两个变量指向同一块堆内存。

深浅拷贝,正是为了解决复杂数据类型在复制时"要不要断开原对象引用"的问题而诞生的。

一、 浅拷贝

1. 什么是浅拷贝?

浅拷贝会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

  • 如果属性是简单类型 ,拷贝的就是基本类型的值
  • 如果属性是复杂类型(引用类型) ,拷贝的就是内存地址。因此,如果其中一个对象改变了这个地址指向的对象,就会影响到另一个对象。

简单来说:浅拷贝只复制了对象的第一层结构,更深层次的引用类型依然共享同一个堆内存。

2. 常见实现方式

在 JS 中,以下操作都属于浅拷贝:

Object.assign()

JavaScript 复制代码
const original = { name: 'Alice', details: { age: 25 } };
const copy = Object.assign({}, original);

copy.name = 'Bob';         // 修改第一层(简单类型)
copy.details.age = 30;     // 修改第二层(引用类型)

console.log(original.name);        // 'Alice' (没变,说明第一层隔离了)
console.log(original.details.age); // 30      (变了!说明深层依然共享)

扩展运算符(...)

JavaScript 复制代码
const arr1 = [1, 2, { role: 'admin' }];
const arr2 = [...arr1];

arr2[2].role = 'user';
console.log(arr1[2].role); // 'user' (原数组被连锁修改)

数组方法 Array.prototype.slice() / concat()

JavaScript 复制代码
const originalArr = [[1, 2], 3];
const copyArr = originalArr.slice();
copyArr[0][0] = 99;
console.log(originalArr[0][0]); // 99

二、 深拷贝

1. 什么是深拷贝?

深拷贝会开辟一块完全独立的堆内存空间,不仅复制目标对象的第一层属性,还会递归地将对象中所有层级的引用类型属性全部复制一遍,直到全都是简单数据类型为止。

简单来说:深拷贝实现修改新对象绝对不会影响旧对象。

2. 常见实现方式

投机取巧法:JSON.parse(JSON.stringify())

这是开发中最常用的快捷方式。先将对象转成字符串(此时断开了所有引用),再转回对象(重新在堆中开辟空间)。

JavaScript 复制代码
const original = { name: 'Alice', details: { age: 25 } };
const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.details.age = 30;
console.log(original.details.age); // 25 (成功隔离,原对象没变!)

⚠️ 致命缺陷:

  • 会忽略 undefinedSymbolFunction
  • 遇到 RegExp(正则表达式)会变成空对象 {}
  • 遇到 Date 对象会变成字符串。
  • 无法解决循环引用(对象内部属性引用了对象自己,会导致报错)。

现代标准解法:structuredClone()

现代浏览器和 Node.js(v17+)内置的原生深拷贝 API。它克服了 JSON 方法的绝大多数缺陷。

JavaScript 复制代码
const original = { 
  date: new Date(), 
  reg: /abc/g,
  details: { age: 25 } 
};
const deepCopy = structuredClone(original);

console.log(deepCopy.date instanceof Date); // true (保留了内置对象类型)

⚠️ 局限: 依然不能拷贝 Function 和 DOM 节点。

手写递归深拷贝

JavaScript 复制代码
function deepClone(obj, hash = new WeakMap()) {
    // 1. 如果是基本数据类型(String, Number, Boolean, Undefined, Symbol, BigInt)或者是 null
    // 直接返回其值,因为它们在栈中赋值本身就是安全的
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    // 2. 处理特殊的内置引用对象:Date 和 RegExp
    // 如果直接遍历它们,会丢失原本的日期值或正则规则,变成空对象
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);

    // 3. 解决循环引用(Circular Reference)
    // 如果这个对象之前已经被拷贝过一次,直接去 hash 缓存表里拿出来返回,不再重复拷贝
    if (hash.has(obj)) {
        return hash.get(obj);
    }

    // 4. 初始化克隆对象
    // 利用 obj.constructor() 可以动态保持原对象的原型链和类型(自动区分是 {} 还是 [])
    const cloneObj = new obj.constructor();

    // 5. 将当前对象和对应的克隆对象存入 WeakMap 缓存表中
    // 必须在开始遍历子属性之前存入,否则遇到循环引用时无法命中缓存
    hash.set(obj, cloneObj);

    // 6. 遍历对象的所有属性(包括普通的 String 键和特殊的 Symbol 键)
    // Reflect.ownKeys() 可以完美替代 Object.keys() + Object.getOwnPropertySymbols()
    Reflect.ownKeys(obj).forEach(key => {
        // 递归调用 deepClone:如果子属性还是对象,就继续深拷贝;如果是基本类型,直接赋值
        // 同时要把 hash 表继续往下传递
        cloneObj[key] = deepClone(obj[key], hash);
    });

    // 7. 返回拷贝完成的对象
    return cloneObj;
}

测试代码

JavaScript 复制代码
// 1. 构造一个包含各种奇葩类型的测试对象
const target = {
    num: 1,
    str: 'hello',
    bool: true,
    undef: undefined,
    nullVal: null,
    date: new Date(),
    reg: /^\d+$/gi,
    arr: [10, 20, { score: 100 }],
    obj: { name: '内部对象' },
    [Symbol('id')]: '这是一个Symbol属性' // Symbol 键
};

// 2. 制造循环引用:让 target 的 circle 属性指向它自己
target.circle = target;

// 3. 执行深拷贝
const result = deepClone(target);

// 4. 验证隔离性与正确性
result.arr[2].score = 999;
result.obj.name = '被修改了';

console.log(target.arr[2].score); // 100 ------ 原对象没变,说明数组内部的对象成功断开引用!
console.log(target.obj.name);     // '内部对象' ------ 成功隔离!
console.log(result.circle === result); // true ------ 成功处理了循环引用,没有死循环报错!
console.log(result.date instanceof Date); // true ------ 正确保留了 Date 类型

总结

正是因为复杂数据类型存储的是堆内存地址,才导致了我们在复制时需要区分"仅仅复制地址(浅拷贝)"还是"复制整座仓库(深拷贝)"。

  • 性能启示 :由于深拷贝需要递归遍历并频繁在堆内存中开辟新空间,它的内存和时间开销远大于浅拷贝。因此,在处理海量数据时,除非确有必要,否则应当尽量避免滥用深拷贝。
相关推荐
IT策士1 小时前
第45篇 k8s之实战:将 Web 应用迁移到 Kubernetes(下)
前端·容器·kubernetes
云水一下1 小时前
TypeScript 从零基础到精通(二):基础类型与类型系统
javascript·typescript
嵌入式ZYXC1 小时前
第1篇:《面试题:画一个STM32最小系统电路,每个元件的作用》
stm32·单片机·嵌入式硬件·面试·职场和发展
你怎么知道我是队长2 小时前
CRC校验C语言实现-CRC8、CRC16、CRC16的直接计算法、查表法
c语言·前端·javascript
Rain5092 小时前
mini-cc 终端 UI:用 React 写 CLI 是什么体验
前端·人工智能·react.js·ui·架构·前端框架·ai编程
wu8587734572 小时前
向量数据库不是银弹:从枚举漏检到 ReACT 多轮召回的实践路径
前端·数据库·react.js
meilindehuzi_a2 小时前
深入理解 JavaScript 执行机制:从编译阶段到调用栈底层实现
开发语言·javascript·ecmascript
古怪今人2 小时前
[前端]HTML盒模型与尺寸,标准文档流,块级元素、内联元素和行内块,CSS选择器
前端·css
小雨下雨的雨3 小时前
基于鸿蒙PC Electron框架技术完成的表单验证技术详解
前端·javascript·华为·electron·前端框架·鸿蒙