Skip to content

深浅拷贝

在 JavaScript 中,数据拷贝是日常开发中频繁遇到的操作。但很多开发者在面对"拷贝" 时,常会陷入深浅拷贝的误区——明明复制了对象,却发现修改新对象时原对象也跟着变了。这就像复印文件时,浅拷贝只复印了封面,深拷贝才真正把内页也完整复制。今天我们就来彻底搞懂这两种拷贝方式,以及它们的实现方法。

浅拷贝

浅拷贝就像给物品套了个新盒子——只复制对象的表层结构,对于嵌套的子对象,新旧对象会共享同一份内存地址 。这意味着修改子对象时,原对象和拷贝对象会同时受到影响。

Object.assign()

Object.assign() 是 ES6 引入的浅拷贝方法,它会将源对象的可枚举属性复制到目标对象,并返回目标对象。

javascript
const obj = {
    name: "张三",
    info: {age: 20}
};

// 浅拷贝:只复制表层属性
const copyObj = Object.assign({}, obj);

// 修改表层属性,原对象不受影响
copyObj.name = "李四";
console.log(obj.name); // 输出:张三(原对象不变)

// 修改嵌套对象,原对象跟着变
copyObj.info.age = 21;
console.log(obj.info.age); // 输出:21(原对象被修改)

原理Object.assign() 会遍历源对象的自有属性,将其值复制到目标对象。但对于引用类型的属性(如例子中的 info 对象),复制的是内存地址,而非对象本身——这就像两个盒子共用了同一个抽屉。

注意

  • 只拷贝自身属性,不拷贝继承属性和不可枚举属性
  • 如果目标对象与源对象有同名属性,源对象属性会覆盖目标对象属性

结构符号(扩展运算符)

扩展运算符(...)是一种更简洁的浅拷贝方式,语法上比 Object.assign() 更直观,常用于数组和对象的拷贝。

javascript
// 拷贝对象
const obj = {a: 1, b: {c: 2}};
const copyObj = {...obj};

// 拷贝数组
const arr = [1, 2, {3}];
const copyArr = [...arr];

// 测试嵌套对象
copyObj.b.c = 3;
console.log(obj.b.c); // 输出:3(原对象被修改)

原理:扩展运算符在拷贝时,会对对象/数组进行"一层遍历" ,将每个元素的值复制到新结构中。对于基本类型(数字、字符串等)会复制值,对于引用类型(对象、数组等)会复制地址——就像用相机拍集体照,只能拍到每个人的外表,拍不到口袋里的东西。

适用场景

  • 简单数据结构的快速拷贝
  • 合并多个对象(如 {...obj1, ...obj2}
  • 函数参数的解构传递

深拷贝

深拷贝是"彻底的复制"——**不仅复制对象的表层结构,还会递归复制所有嵌套的子对象,新旧对象完全独立,修改任何一方都不会影响另一方 **。就像细胞分裂,新细胞拥有和母细胞完全相同的遗传物质,但之后会各自发展。

JSON.stringify()

JSON.stringify() 配合 JSON.parse() 是最常用的深拷贝方案,通过将对象转为 JSON 字符串,再解析回对象,实现完全独立的复制。

javascript
const obj = {
    name: "张三",
    info: {age: 20},
    hobbies: ["篮球", "游戏"]
};

// 深拷贝:先转JSON字符串,再解析回对象
const deepCopy = JSON.parse(JSON.stringify(obj));

// 修改嵌套对象,原对象不受影响
deepCopy.info.age = 21;
deepCopy.hobbies.push("读书");
console.log(obj.info.age); // 输出:20(原对象不变)
console.log(obj.hobbies); // 输出:["篮球", "游戏"](原数组不变)

原理:JSON 字符串是纯文本格式,转换过程会剥离对象的引用关系。当重新解析为对象时,浏览器会为所有嵌套结构分配新的内存地址,实现彻底的独立。

局限性

  • 无法拷贝函数、正则表达式、日期对象(会被转为字符串)
  • 无法处理循环引用(如 obj.self = obj 会报错)
  • 会忽略 undefinedSymbol 类型的属性

MessageChannel(结构化克隆)

MessageChannel 是浏览器提供的通信 API,其内部使用"结构化克隆算法",能处理 JSON.stringify 无法拷贝的复杂类型(如循环引用、函数除外)。

javascript
function deepClone(obj) {
    return new Promise((resolve) => {
        const {port1, port2} = new MessageChannel();
        // 通过端口发送对象,会自动触发结构化克隆
        port2.onmessage = (e) => resolve(e.data);
        port1.postMessage(obj);
    });
}

// 使用示例
const obj = {
    name: "张三",
    date: new Date(),
    self: null // 准备循环引用
};
obj.self = obj; // 循环引用:对象引用自身

// 注意:需要用async/await调用
(async () => {
    const copy = await deepClone(obj);
    console.log(copy.self === copy); // 输出:true(保留循环引用)
    console.log(copy.date instanceof Date); // 输出:true(正确拷贝日期对象)
})();

原理MessageChannelpostMessage 方法在传递数据时,会使用浏览器内置的结构化克隆算法,递归复制对象的所有层级,并保留复杂类型的特性。就像专业的文件复制工具,能识别并复制各种格式的文件。

支持的类型

  • 基本类型(除 Symbol
  • 日期、正则表达式
  • 数组、对象
  • 循环引用(对象引用自身)

局限性

  • 无法拷贝函数(会被忽略)
  • 是异步操作,需要配合 Promise 使用
  • 仅支持浏览器环境(Node.js 不支持)

手写深拷贝

实际开发中,我们可能需要一个自定义的深拷贝函数,灵活处理各种场景。手写深拷贝的核心是**递归遍历对象,对不同类型的数据采用不同的复制策略 **。

javascript
function deepClone(obj, hash = new WeakMap()) {
    // 处理null和基本类型
    if (obj === null || typeof obj !== "object") {
        return obj;
    }

    // 处理循环引用
    if (hash.has(obj)) {
        return hash.get(obj);
    }

    let copy;

    // 处理日期
    if (obj instanceof Date) {
        copy = new Date(obj);
        hash.set(obj, copy);
        return copy;
    }

    // 处理正则
    if (obj instanceof RegExp) {
        copy = new RegExp(obj.source, obj.flags);
        hash.set(obj, copy);
        return copy;
    }

    // 处理数组和对象
    copy = Array.isArray(obj) ? [] : {};
    hash.set(obj, copy); // 缓存已拷贝的对象,解决循环引用

    // 递归拷贝属性
    Reflect.ownKeys(obj).forEach(key => {
        copy[key] = deepClone(obj[key], hash);
    });

    return copy;
}

// 测试
const obj = {
    a: 1,
    b: {c: 2},
    d: new Date(),
    e: /test/g,
    self: null
};
obj.self = obj;

const copy = deepClone(obj);
console.log(copy.b.c); // 2
console.log(copy.self === copy); // true(正确处理循环引用)
console.log(copy.d instanceof Date); // true

核心逻辑

  1. 类型判断:区分基本类型、对象、数组、日期等特殊类型
  2. 递归拷贝:对对象的每个属性递归调用深拷贝函数
  3. 循环引用处理:用 WeakMap 缓存已拷贝的对象,避免无限递归
  4. 特殊类型处理:为日期、正则等对象创建新实例,保留其特性

优势

  • 可自定义处理函数等特殊类型(如需拷贝函数,可添加 typeof obj === 'function' 的处理逻辑)
  • 同步操作,无需异步等待
  • 全环境兼容(浏览器和 Node.js 均可使用)

总结

深浅拷贝的本质区别在于是否复制嵌套对象的引用

  • 浅拷贝:适合简单数据结构,效率高,但嵌套对象会共享引用
  • 深拷贝:适合复杂数据结构,完全独立,但性能消耗更大

选择建议:

  • 简单场景用 Object.assign() 或扩展运算符
  • 需处理日期、循环引用时用 MessageChannel
  • 需兼容函数、正则或自定义逻辑时,使用手写深拷贝
  • 避免用 JSON.stringify 处理包含特殊类型的对象

掌握深浅拷贝,能帮你避免很多"修改新对象却影响原对象"的诡异 bug,让数据操作更可控。就像学会了不同的复制技巧,既能快速复印简单文件,也能完整备份复杂档案。