📝 深入浅出 JavaScript 拷贝:从浅拷贝到深拷贝 🚀

🧩 从无拷贝到拷贝:一个演进的故事

想象你有一本珍贵的笔记本📒,朋友想借阅时,如果你直接把原本给他(无拷贝),那么他做的任何修改都会直接影响你的原始笔记 - 这显然风险很大。于是你决定复印一份(浅拷贝),这时朋友可以在复印件上写写画画,但问题来了:如果你的笔记本里有可拆卸的便利贴📝,朋友把便利贴拿走或修改了,你的原笔记本里的便利贴也会受到影响!最后你找到了完美解决方案:不仅复印笔记本的每一页,还把里面所有的便利贴也一一复印(深拷贝),这样无论朋友怎么修改他的副本,你的原始笔记都能保持完好无损。这就是从无拷贝到浅拷贝再到深拷贝的演进过程!

什么是拷贝?🤔

在 JavaScript 中,拷贝就是创建一个与原始数据值相同但内存独立的新数据。让我们先看看不拷贝会发生什么:

javascript 复制代码
let arr = [1, 2, 3];
let newArr = arr;  // 这只是引用赋值,不是拷贝!
newArr[0] = 100;

console.log(arr); // [100, 2, 3] 😱

这里修改 newArr 影响了 arr,因为它们指向同一个内存地址!

浅拷贝:只拷贝第一层 🏊‍♂️

浅拷贝会创建一个新对象,但只复制对象的第一层属性。如果属性是引用类型,则复制的是引用地址。

5种常用浅拷贝方法:

  1. 手动实现 👷‍♂️
javascript 复制代码
function shallowClone(target) {
  // 1. 处理非对象类型或null值(直接返回)
  if (typeof target !== 'object' || target === null) return target;
  
  // 2. 根据目标类型初始化克隆对象(数组或普通对象)
  const clone = Array.isArray(target) ? [] : {};
  
  // 3. 遍历目标对象的自有属性
  for (let key in target) {
    // 使用hasOwnProperty确保只拷贝对象自身的属性(不拷贝原型链上的属性)
    if (target.hasOwnProperty(key)) {
      clone[key] = target[key];  // 直接复制属性值(浅拷贝)
    }
  }
  return clone;
}

hasOwnProperty()的作用

  • 是JavaScript对象的内置方法
  • 用于检查对象是否自身拥有指定的属性(不包括从原型链继承的属性)
  • 语法:obj.hasOwnProperty(propertyName)
  • 在拷贝时使用可以避免意外拷贝原型链上的属性(如Object.prototype上的方法)
  1. Object.assign() 📦
javascript 复制代码
const obj = { name: '小明', age: 18 };
const newObj = Object.assign({}, obj);
  1. 数组的 concat()
javascript 复制代码
const arr = [1, 2, 3];
const newArr = arr.concat();
  1. 数组的 slice() ✂️
javascript 复制代码
const arr = [1, 2, 3];
const newArr = arr.slice();
  1. 展开运算符 ... 🌈
javascript 复制代码
const arr = [1, 2, 3];
const newArr = [...arr];

const obj = { a: 1, b: 2 };
const newObj = { ...obj };

浅拷贝的局限性 ⚠️

javascript 复制代码
const person = { name: '小明', hobbies: ['篮球', '游泳'] };
const clonePerson = shallowClone(person);

clonePerson.hobbies.push('跑步');
console.log(person.hobbies); // ['篮球', '游泳', '跑步'] 😅

虽然 personclonePerson 是两个对象,但它们共享同一个 hobbies 数组引用!

深拷贝:彻底的内存分离 🏗️

深拷贝会递归复制所有层级,创建一个完全独立的新对象。

简易深拷贝方法(有局限)🚧

javascript 复制代码
const cloned = JSON.parse(JSON.stringify(obj));

⚠️ 这个方法的问题:

  1. ❌无法处理循环引用(会栈溢出)

    javascript 复制代码
    const a = {val:2};
    a.target = a;

    拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  2. ❌ 会丢失特殊对象(Date、RegExp等)

  3. ❌ 无法复制函数

  4. ❌ 会忽略Symbol类型的键

完整版深拷贝实现 🛠️

javascript 复制代码
/**
 * 深度克隆任意JavaScript对象,处理循环引用和各种内置类型
 * @param {*} target - 需要克隆的目标值,可以是任意类型
 * @param {WeakMap} [map=new WeakMap()] - 用于跟踪已克隆对象的WeakMap,解决循环引用问题
 * @returns {*} 返回深度克隆后的副本
 * @throws {Error} 对于明确不支持克隆的类型可能会抛出错误(根据配置)
 */
function deepClone(target, map = new WeakMap()) {
  // 基本类型直接返回(包括:string, number, boolean, null, undefined, symbol, bigint)
  // 函数也直接返回(函数通常不需要也不应该被深拷贝)
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  
  // 处理循环引用:如果已经克隆过该对象,则直接返回之前克隆的结果
  // 这是解决循环引用的关键,避免无限递归
  if (map.has(target)) {
    return map.get(target);
  }
  
  // 使用Object.prototype.toString方法获取精确的对象类型
  // 比typeof和instanceof更可靠,能区分各种内置对象类型
  const type = Object.prototype.toString.call(target);
  
  // 根据不同类型采用不同的克隆策略
  switch (type) {
    case '[object Date]':
      // Date对象:通过时间戳创建新实例
      // 确保克隆后的对象与原始对象时间相同但独立
      return new Date(target.getTime());
      
    case '[object RegExp]': {
      // 正则表达式处理
      const regFlags = /\w*$/; // 匹配标志的正则表达式(g, i, m等)
      // 创建新正则,复制源表达式和标志
      const result = new RegExp(target.source, regFlags.exec(target));
      // 复制lastIndex属性(记录正则匹配位置状态)
      result.lastIndex = target.lastIndex;
      return result;
    }
    
    case '[object Map]': {
      // Map处理
      const result = new Map();
      // 先存入WeakMap,再填充内容,解决循环引用
      map.set(target, result);
      // 遍历原始Map,递归克隆key和value
      target.forEach((val, key) => {
        result.set(deepClone(key, map), deepClone(val, map));
      });
      return result;
    }
    
    case '[object Set]': {
      // Set处理
      const result = new Set();
      map.set(target, result);
      // 遍历原始Set,递归克隆每个值
      target.forEach(val => {
        result.add(deepClone(val, map));
      });
      return result;
    }
    
    case '[object Array]': {
      // 数组处理
      const result = [];
      map.set(target, result);
      // 遍历数组,递归克隆每个元素
      target.forEach((item, index) => {
        result[index] = deepClone(item, map);
      });
      return result;
    }
    
    case '[object Object]': {
      // 普通对象处理(包括自定义类实例)
      // 获取对象的所有自身属性(包括Symbol属性)
      const keys = Reflect.ownKeys(target);
      const result = {};
      map.set(target, result);
      
      // 遍历所有属性
      keys.forEach(key => {
        // 只克隆可枚举属性(与JSON.stringify行为一致)
        if (target.propertyIsEnumerable(key)) {
          result[key] = deepClone(target[key], map);
        }
      });
      
      // 保持原型链:将克隆对象的原型设置为原对象的原型
      // 这使得克隆后的自定义类实例仍然能访问原型方法
      Object.setPrototypeOf(result, Object.getPrototypeOf(target));
      return result;
    }
    
    default:
      // 对于其他未处理的特殊对象(如Error, Promise, WeakMap, WeakSet等)
      // 返回原对象是最安全的做法,因为这些对象:
      // 1. 可能有特殊内部状态无法克隆
      // 2. 克隆后可能破坏其正常功能
      // 也可以选择抛出错误让调用者明确知道不支持
      return target;
      // 或者: throw new Error(`Unsupported type: ${type}`);
  }
}

使用示例 🌟

javascript 复制代码
const original = {
  name: '小明',
  age: 18,
  hobbies: ['篮球', '游泳'],
  birth: new Date('2000-01-01'),
  friends: new Set(['小红', '小刚']),
  metadata: new Map([['id', 123]]),
  sayHi: function() { console.log('Hi!'); }
};

// 添加循环引用
original.self = original;

const cloned = deepClone(original);

console.log(cloned !== original); // true ✅
console.log(cloned.hobbies !== original.hobbies); // true ✅
console.log(cloned.self === cloned); // true ✅ 循环引用正确处理

总结 📚

特性 浅拷贝 🏊‍♂️ 深拷贝 🏗️
拷贝层级 仅第一层 所有层级
引用类型处理 共享引用 完全独立
性能 快 ⚡ 慢 🐢
适用场景 简单对象 复杂嵌套对象

选择拷贝方式时,要根据实际需求权衡:

  • 如果确定对象结构简单,使用浅拷贝性能更好
  • 如果对象有嵌套结构或需要完全隔离,使用深拷贝更安全

希望这篇博客能帮助你理解 JavaScript 中的拷贝机制!🎉 如果有任何问题,欢迎留言讨论~ 💬

相关推荐
中微子4 分钟前
React 状态管理 源码深度解析
前端·react.js
加减法原则1 小时前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele2 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4532 小时前
React移动端开发项目优化
前端·react.js·前端框架
你的人类朋友2 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir2 小时前
vue3.2 前端动态分页算法
前端·算法
烛阴2 小时前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子2 小时前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js
DoraBigHead3 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构
Xiaouuuuua4 小时前
一个简单的脚本,让pdf开启夜间模式
java·前端·pdf