JavaScript浅拷贝与深拷贝

目录

浅拷贝(Shallow Copy)

一、浅拷贝的定义

二、直接赋值 vs 浅拷贝

1. 直接赋值

2. 浅拷贝

三、数组的浅拷贝方法

1. slice()

2. concat()

3. 扩展运算符(...)

四、对象的浅拷贝方法

1. Object.assign()

2. 扩展运算符(...)

五、浅拷贝的局限性

六、总结

深拷贝(Deep Copy)

一、深拷贝的定义

二、深拷贝的常见实现方式

1. JSON.parse(JSON.stringify())

2. 递归手动实现

3. 第三方库(如 Lodash 的 _.cloneDeep())

4. MessageChannel(异步深拷贝)

三、深拷贝的边界条件处理

1. 循环引用

2. 特殊对象类型

四、总结

五、浅拷贝 vs 深拷贝对比

六、_.cloneDeep(value) 源码解析

1. 核心逻辑

2. 关键源码步骤

3. 核心特性


浅拷贝(Shallow Copy)


一、浅拷贝的定义

  • 核心概念:创建一个新对象,仅复制原始对象的 第一层属性值。若属性为 基本类型,拷贝其值;若为 引用类型(如对象、数组),则拷贝其内存地址(共享同一引用)。

  • 特点

    • 修改新对象的 引用类型属性 会影响原对象。

    • 修改新对象的 基本类型属性 不会影响原对象。


二、直接赋值 vs 浅拷贝

1. 直接赋值

  • 本质:将变量指向原对象的 内存地址,未创建新对象。

  • 结果:新旧变量完全共享同一对象,任意修改均相互影响。

    const obj = { a: 1, b: { c: 2 } };
    const copy = obj; // 直接赋值
    copy.a = 10;
    copy.b.c = 20;
    
    console.log(obj.a);      // 10(基本类型也被修改)
    console.log(obj.b.c);    // 20

2. 浅拷贝

  • 本质:创建新对象,复制原对象的 第一层属性值

  • 结果

    • 基本类型属性:独立修改,互不影响。

    • 引用类型属性:共享同一内存地址,修改相互影响。

    const obj = { a: 1, b: { c: 2 } };
    const copy = { ...obj }; // 浅拷贝(扩展运算符)
    copy.a = 10;             // 修改基本类型属性
    copy.b.c = 20;           // 修改引用类型属性
    
    console.log(obj.a);      // 1(原对象未变)
    console.log(obj.b.c);    // 20(原对象被修改)

三、数组的浅拷贝方法

1. slice()

const arr = [1, 2, { a: 3 }];
const copy = arr.slice(); // 浅拷贝数组

copy[0] = 10;            // 修改基本类型元素
copy[2].a = 30;          // 修改引用类型元素

console.log(arr[0]);     // 1(原数组未变)
console.log(arr[2].a);   // 30(原数组被修改)

2. concat()

const arr = [1, 2, { a: 3 }];
const copy = arr.concat(); // 浅拷贝数组

3. 扩展运算符(...

const arr = [1, 2, { a: 3 }];
const copy = [...arr]; // 浅拷贝数组

四、对象的浅拷贝方法

1. Object.assign()

const obj = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, obj); // 浅拷贝对象

copy.a = 10;        // 修改基本类型属性
copy.b.c = 20;      // 修改引用类型属性

console.log(obj.a); // 1(原对象未变)
console.log(obj.b.c); // 20(原对象被修改)

2. 扩展运算符(...

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

五、浅拷贝的局限性

  • 嵌套引用类型:浅拷贝无法处理多层级对象或数组中的引用类型属性。

    const original = { a: 1, b: { c: 2 }, d: [3] };
    const copy = { ...original };
    
    copy.b.c = 20; // 原对象被修改
    copy.d.push(4); // 原数组被修改
    console.log(original.b.c); // 20
    console.log(original.d);   // [3, 4]

六、总结

  • 浅拷贝:复制对象的第一层属性,适用于简单数据结构。

  • 直接赋值:共享内存地址,无独立副本。

  • 数组浅拷贝slice()concat()、扩展运算符。

  • 对象浅拷贝Object.assign()、扩展运算符。

  • 深层拷贝需求:需使用深拷贝方法。


   

深拷贝(Deep Copy)


一、深拷贝的定义

  • 核心概念:创建一个新对象,递归复制原始对象的所有层级属性(包括嵌套对象和数组),新对象与原对象完全独立,修改互不影响。

  • 特点

    • 完全独立内存:所有层级的引用类型属性均为新对象。

    • 适用场景:复杂嵌套对象、需完全隔离数据的场景(如状态管理、数据快照)。


二、深拷贝的常见实现方式

1. JSON.parse(JSON.stringify())

  • 原理:将对象序列化为 JSON 字符串,再解析为新的对象。

  • 示例

    const obj = { a: 1, b: { c: 2 }, d: [3] };
    const copy = JSON.parse(JSON.stringify(obj));
    copy.b.c = 20;
    console.log(obj.b.c); // 2(原对象未变)
  • 优点:简单快捷,适用于大多数 JSON 安全的数据。

  • 缺点

    • 不支持特殊类型:忽略 undefinedSymbol、函数、循环引用。

    • 破坏原型链:无法复制对象的构造函数和原型方法。

    • 日期对象:会被转换为 ISO 字符串,反序列化为字符串而非 Date 对象。


2. 递归手动实现

  • 原理:递归遍历对象属性,逐层复制基本类型和引用类型。

  • 示例

简易版:

function deepCopy(newObj, oldObj) {
  debugger  //类似于在代码中设置断点
  for (let k in oldObj) {
    // 处理数组的问题  一定先写数组再写对象 不能颠倒
    if (oldObj[k] instanceof Array) {
      newObj[k] = []
      deepCopy(newObj[k], oldObj[k])
    } 
    else if (oldObj[k] instanceof Object) {
      newObj[k] = {}
      deepCopy(newObj[k], oldObj[k])
    }
    else {
      newObj[k] = oldObj[k]
    }
  }
}

升级版: 

function deepClone(source, map = new WeakMap()) {
  // 处理基本类型和 null
  if (source === null || typeof source !== 'object') return source;

  // 处理循环引用
  if (map.has(source)) return map.get(source);
  
  // 处理数组和对象
  const target = Array.isArray(source) ? [] : {};
  map.set(source, target);

  // 递归复制属性
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      target[key] = deepClone(source[key], map);
    }
  }
  return target;
}

// 使用示例
const obj = { a: 1, b: { c: 2 }, d: [3] };
const copy = deepClone(obj);
copy.b.c = 20;
console.log(obj.b.c); // 2(原对象未变)
  1. 优点

    • 支持所有数据类型(需扩展代码处理 DateRegExpMapSet 等)。

    • 可处理循环引用(通过 WeakMap 缓存)。

  • 缺点

    • 实现复杂,需处理多种边界情况。

    • 性能较低(递归遍历耗时)。


3. 第三方库(如 Lodash 的 _.cloneDeep()

  • 原理:成熟的深拷贝实现,处理了各种数据类型和边界条件。

  • 示例

    const _ = require('lodash');
    const obj = { a: 1, b: { c: 2 }, d: [3] };
    const copy = _.cloneDeep(obj);
    copy.b.c = 20;
    console.log(obj.b.c); // 2
  • 优点

    • 功能全面,支持复杂类型(如 DateRegExpMapSet)。

    • 高性能且经过严格测试。

  • 缺点:需引入外部库。


4. MessageChannel(异步深拷贝)

  • 原理:利用浏览器 API 的通信机制实现深拷贝。

  • 示例

    function deepClone(obj) {
      return new Promise(resolve => {
        const { port1, port2 } = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
      });
    }
    
    // 使用示例(异步)
    const obj = { a: 1, b: { c: 2 } };
    deepClone(obj).then(copy => {
      copy.b.c = 20;
      console.log(obj.b.c); // 2
    });
  • 优点:可处理循环引用和部分特殊对象(如 Date)。

  • 缺点

    • 异步操作,使用不便。

    • 无法复制函数、undefined 等非序列化数据。

    • 仅限浏览器环境。


三、深拷贝的边界条件处理

1. 循环引用

  • 问题:对象属性间接或直接引用自身,导致递归无限循环。

  • 解决:使用 WeakMap 缓存已拷贝对象。

    const obj = { a: 1 };
    obj.self = obj; // 循环引用
    
    const copy = deepClone(obj); // 正确处理

2. 特殊对象类型

  • 处理方式

    • Date:创建新 Date 实例。

    • RegExp:复制正则表达式和标志。

    • Map/Set:递归复制其内容。

    // 扩展递归函数处理 Date
    if (source instanceof Date) return new Date(source);

四、总结

方法推荐场景注意事项
JSON 方法简单 JSON 安全数据(无特殊类型)忽略函数、undefined、循环引用
递归手动实现需要完全控制深拷贝逻辑需扩展处理特殊类型和循环引用
Lodash.cloneDeep生产环境,复杂数据类型引入外部依赖
MessageChannel浏览器环境,支持部分特殊对象异步操作,无法复制函数

最佳实践

  • 优先使用 Lodash 等成熟库:减少重复造轮子和潜在错误。

  • 手动实现需谨慎:充分测试循环引用、特殊数据类型和性能。

  • 避免滥用深拷贝:在数据隔离需求明确时使用,避免不必要的性能损耗。


五、浅拷贝 vs 深拷贝对比

特性浅拷贝深拷贝
复制层级仅复制第一层属性递归复制所有层级属性
内存占用低(共享引用类型属性)高(完全独立副本)
性能高(仅复制第一层)较低(递归遍历耗时)
适用场景简单对象或无需隔离引用属性的场景需要完全隔离数据的场景
实现复杂度低(简单复制)高(需处理循环引用、特殊类型)

六、_.cloneDeep(value) 源码解析

1. 核心逻辑

_.cloneDeep 用于深拷贝任意类型的数据,包括对象、数组、DateRegExpMapSet 等。其核心实现依赖于递归遍历和类型判断。

2. 关键源码步骤
function cloneDeep(value) {
  return baseClone(value, true /* deep */);
}

// 基础克隆函数(Lodash 内部实现)
function baseClone(value, isDeep, customizer, key, object, stack) {
  // 使用栈跟踪循环引用
  stack || (stack = new Stack());
  
  // 处理已拷贝的对象(避免循环引用)
  if (stack.has(value)) {
    return stack.get(value);
  }

  let result;
  // 根据类型选择克隆策略
  if (isObject(value)) {
    // 处理数组、对象、Map、Set、Date、RegExp 等
    const tag = getTag(value);
    switch (tag) {
      case '[object Array]':
        result = initCloneArray(value);
        break;
      case '[object Date]':
        result = new Date(value.getTime());
        break;
      case '[object Map]':
        result = new Map();
        value.forEach((subValue, key) => {
          result.set(key, baseClone(subValue, isDeep, customizer, key, value, stack));
        });
        break;
      // 其他类型处理(如 Set、RegExp 等)
      // ...
    }
    // 注册到栈中,处理循环引用
    stack.set(value, result);
    // 递归拷贝属性
    copyProperties(value, result, isDeep, customizer, stack);
  } else {
    // 处理基本类型(直接返回)
    result = value;
  }
  return result;
}
3. 核心特性
  • 循环引用处理:使用 Stack 结构(内部基于数组或 WeakMap)记录已拷贝对象。

  • 类型识别:通过 Object.prototype.toString.call(value) 获取精确类型。

  • 递归拷贝:对嵌套结构逐层复制,确保深层次独立性。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值